From 5aa524bef44e7bcf1a9586ebc853e3f8dc989270 Mon Sep 17 00:00:00 2001 From: sosokker Date: Sat, 4 Nov 2023 03:51:20 +0700 Subject: [PATCH] Separate Authentication and Users --- backend/authentications/__init__.py | 0 .../access_token_cache.py | 2 +- backend/{users => authentications}/adapter.py | 0 backend/authentications/admin.py | 3 + backend/authentications/apps.py | 6 + .../authentications/migrations/__init__.py | 0 backend/authentications/models.py | 3 + backend/authentications/serializers.py | 12 ++ backend/authentications/tests.py | 3 + backend/authentications/urls.py | 12 ++ backend/authentications/views.py | 168 ++++++++++++++++++ backend/core/settings.py | 1 + backend/core/urls.py | 1 + backend/tasks/utils.py | 2 +- backend/users/serializers.py | 12 -- backend/users/urls.py | 9 +- backend/users/views.py | 164 +---------------- 17 files changed, 215 insertions(+), 183 deletions(-) create mode 100644 backend/authentications/__init__.py rename backend/{users => authentications}/access_token_cache.py (98%) rename backend/{users => authentications}/adapter.py (100%) create mode 100644 backend/authentications/admin.py create mode 100644 backend/authentications/apps.py create mode 100644 backend/authentications/migrations/__init__.py create mode 100644 backend/authentications/models.py create mode 100644 backend/authentications/serializers.py create mode 100644 backend/authentications/tests.py create mode 100644 backend/authentications/urls.py create mode 100644 backend/authentications/views.py diff --git a/backend/authentications/__init__.py b/backend/authentications/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/users/access_token_cache.py b/backend/authentications/access_token_cache.py similarity index 98% rename from backend/users/access_token_cache.py rename to backend/authentications/access_token_cache.py index 37dada0..049b08e 100644 --- a/backend/users/access_token_cache.py +++ b/backend/authentications/access_token_cache.py @@ -5,7 +5,7 @@ from google.auth.transport.requests import Request from google.oauth2.credentials import Credentials from googleapiclient.discovery import build -from .models import CustomUser +from users.models import CustomUser def store_token(user_id, token, token_type): diff --git a/backend/users/adapter.py b/backend/authentications/adapter.py similarity index 100% rename from backend/users/adapter.py rename to backend/authentications/adapter.py diff --git a/backend/authentications/admin.py b/backend/authentications/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/backend/authentications/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/authentications/apps.py b/backend/authentications/apps.py new file mode 100644 index 0000000..1747d6d --- /dev/null +++ b/backend/authentications/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AuthenticationsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'authentications' diff --git a/backend/authentications/migrations/__init__.py b/backend/authentications/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/authentications/models.py b/backend/authentications/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/backend/authentications/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/backend/authentications/serializers.py b/backend/authentications/serializers.py new file mode 100644 index 0000000..b288524 --- /dev/null +++ b/backend/authentications/serializers.py @@ -0,0 +1,12 @@ +from rest_framework_simplejwt.serializers import TokenObtainPairSerializer + + +class MyTokenObtainPairSerializer(TokenObtainPairSerializer): + @classmethod + def get_token(cls, user): + """ + Get the token for the user and add custom claims, such as 'username'. + """ + token = super(MyTokenObtainPairSerializer, cls).get_token(user) + token['username'] = user.username + return token \ No newline at end of file diff --git a/backend/authentications/tests.py b/backend/authentications/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/authentications/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/authentications/urls.py b/backend/authentications/urls.py new file mode 100644 index 0000000..32965a4 --- /dev/null +++ b/backend/authentications/urls.py @@ -0,0 +1,12 @@ +from django.urls import path +from rest_framework_simplejwt import views as jwt_views +from authentications.views import ObtainTokenPairWithCustomView, GreetingView, GoogleLogin, GoogleRetrieveUserInfo + +urlpatterns = [ + path('token/obtain/', jwt_views.TokenObtainPairView.as_view(), name='token_create'), + path('token/refresh/', jwt_views.TokenRefreshView.as_view(), name='token_refresh'), + path('token/custom_obtain/', ObtainTokenPairWithCustomView.as_view(), name='token_create_custom'), + path('hello/', GreetingView.as_view(), name='hello_world'), + path('dj-rest-auth/google/', GoogleLogin.as_view(), name="google_login"), + path('auth/google/', GoogleRetrieveUserInfo.as_view()), +] \ No newline at end of file diff --git a/backend/authentications/views.py b/backend/authentications/views.py new file mode 100644 index 0000000..9b5f249 --- /dev/null +++ b/backend/authentications/views.py @@ -0,0 +1,168 @@ +from django.shortcuts import render + +# Create your views here. +"""This module defines API views for authentication, user creation, and a simple hello message.""" + +import json +import requests + +from django.conf import settings +from django.contrib.auth.hashers import make_password + +from rest_framework import status +from rest_framework.permissions import IsAuthenticated, AllowAny +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework_simplejwt.tokens import RefreshToken + +from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter + +from dj_rest_auth.registration.views import SocialLoginView + +from google_auth_oauthlib.flow import InstalledAppFlow + +from authentications.access_token_cache import store_token +from authentications.serializers import MyTokenObtainPairSerializer +from users.managers import CustomAccountManager +from users.models import CustomUser + + +class ObtainTokenPairWithCustomView(APIView): + """ + Custom Token Obtain Pair View. + Allows users to obtain access and refresh tokens by providing credentials. + """ + permission_classes = (AllowAny,) + + def post(self, request): + """ + Issue access and refresh tokens in response to a valid login request. + """ + serializer = MyTokenObtainPairSerializer(data=request.data) + if serializer.is_valid(): + token = serializer.validated_data + return Response(token, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class GreetingView(APIView): + """ + Hello World View. + Returns a greeting and user information for authenticated users. + """ + permission_classes = (IsAuthenticated,) + + def get(self, request): + """ + Retrieve a greeting message and user information. + """ + user = request.user + user_info = { + "username": user.username, + } + response_data = { + "message": "Hello, world!", + "user_info": user_info, + } + return Response(response_data, status=status.HTTP_200_OK) + + +class GoogleLogin(SocialLoginView): + """ + Google Login View. + Handles Google OAuth2 authentication. + """ + # permission_classes = (AllowAny,) + adapter_class = GoogleOAuth2Adapter + # client_class = OAuth2Client + # callback_url = 'http://localhost:8000/accounts/google/login/callback/' + + +class GoogleRetrieveUserInfo(APIView): + """ + Retrieve user information from Google and create a user if not exists. + """ + permission_classes = (AllowAny,) + client_config = {"web":{"client_id": settings.GOOGLE_CLIENT_ID, + "project_id":"turtask","auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_secret": settings.GOOGLE_CLIENT_SECRET, + } + } + scopes = [ + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/userinfo.profile', + 'https://www.googleapis.com/auth/calendar.readonly', + ] + + def post(self, request): + code = request.data.get("code") + payload = self.exchange_authorization_code(code=code) + if 'error' in payload: + return Response({'error': payload['error']}) + user_info = self.call_google_api(api_url='https://www.googleapis.com/oauth2/v2/userinfo?alt=json', + access_token=payload['access_token']) + payload['email'] = user_info['email'] + user = self.get_or_create_user(payload) + token = RefreshToken.for_user(user) + + response = { + 'username': user.username, + 'access_token': str(token.access_token), + 'refresh_token': str(token), + } + + return Response(response) + + def get(self, request): + """Get authorization url.""" + flow = InstalledAppFlow.from_client_config(client_config=self.client_config, + scopes=self.scopes) + flow.redirect_uri = 'http://localhost:5173/' + authorization_url, state = flow.authorization_url( + access_type='offline', + # include_granted_scopes='true', + ) + return Response({'url': authorization_url}) + + def exchange_authorization_code(self, code): + """Exchange authorization code for access, id, refresh token.""" + url = 'https://oauth2.googleapis.com/token' + payload = { + 'code': code, + 'client_id': settings.GOOGLE_CLIENT_ID, + 'client_secret': settings.GOOGLE_CLIENT_SECRET, + 'redirect_uri': 'postmessage', + 'grant_type': 'authorization_code', + } + response = requests.post(url, data=payload) + return json.loads(response.text) + + def get_or_create_user(self, user_info): + """Get or create a user based on email.""" + try: + user = CustomUser.objects.get(email=user_info['email']) + user.refresh_token = user_info['refresh_token'] + user.save() + except CustomUser.DoesNotExist: + user = CustomUser() + user.username = user_info['email'] + user.password = make_password(CustomAccountManager().make_random_password()) + user.email = user_info['email'] + user.refresh_token = user_info['refresh_token'] + user.save() + store_token(user.id, user_info['access_token'], 'access') + store_token(user.id, user_info['id_token'], 'id') + return user + + def call_google_api(self, api_url, access_token): + """Call Google API with access token.""" + headers = { + 'Authorization': f'Bearer {access_token}' + } + + response = requests.get(api_url, headers=headers) + if response.status_code == 200: + return response.json() + raise Exception('Google API Error', response) diff --git a/backend/core/settings.py b/backend/core/settings.py index 504cd01..ba1eb69 100644 --- a/backend/core/settings.py +++ b/backend/core/settings.py @@ -50,6 +50,7 @@ INSTALLED_APPS = [ 'tasks', 'users', + 'authentications', 'corsheaders', diff --git a/backend/core/urls.py b/backend/core/urls.py index 78f3e22..418903f 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -21,5 +21,6 @@ urlpatterns = [ path('admin/', admin.site.urls), path('api/', include('users.urls')), path('api/', include('tasks.urls')), + path('api/', include('authentications.urls')), path('accounts/', include('allauth.urls')), ] \ No newline at end of file diff --git a/backend/tasks/utils.py b/backend/tasks/utils.py index 6440b8b..c55eb4a 100644 --- a/backend/tasks/utils.py +++ b/backend/tasks/utils.py @@ -1,6 +1,6 @@ from googleapiclient.discovery import build -from users.access_token_cache import get_credential_from_cache_token +from authentications.access_token_cache import get_credential_from_cache_token def get_service(request): diff --git a/backend/users/serializers.py b/backend/users/serializers.py index 14d96be..a25e8ec 100644 --- a/backend/users/serializers.py +++ b/backend/users/serializers.py @@ -1,19 +1,7 @@ -from rest_framework_simplejwt.serializers import TokenObtainPairSerializer from rest_framework import serializers from .models import CustomUser -class MyTokenObtainPairSerializer(TokenObtainPairSerializer): - @classmethod - def get_token(cls, user): - """ - Get the token for the user and add custom claims, such as 'username'. - """ - token = super(MyTokenObtainPairSerializer, cls).get_token(user) - token['username'] = user.username - return token - - class CustomUserSerializer(serializers.ModelSerializer): """ Serializer for CustomUser model. diff --git a/backend/users/urls.py b/backend/users/urls.py index 28f77da..2b44459 100644 --- a/backend/users/urls.py +++ b/backend/users/urls.py @@ -1,13 +1,6 @@ from django.urls import path -from rest_framework_simplejwt import views as jwt_views -from .views import ObtainTokenPairWithCustomView, CustomUserCreate, GreetingView, GoogleLogin, GoogleRetrieveUserInfo +from users.views import CustomUserCreate urlpatterns = [ path('user/create/', CustomUserCreate.as_view(), name="create_user"), - path('token/obtain/', jwt_views.TokenObtainPairView.as_view(), name='token_create'), - path('token/refresh/', jwt_views.TokenRefreshView.as_view(), name='token_refresh'), - path('token/custom_obtain/', ObtainTokenPairWithCustomView.as_view(), name='token_create_custom'), - path('hello/', GreetingView.as_view(), name='hello_world'), - path('dj-rest-auth/google/', GoogleLogin.as_view(), name="google_login"), - path('auth/google/', GoogleRetrieveUserInfo.as_view()), ] \ No newline at end of file diff --git a/backend/users/views.py b/backend/users/views.py index a448ed2..6f020b6 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -1,46 +1,11 @@ -"""This module defines API views for authentication, user creation, and a simple hello message.""" - -import json -import requests - -from django.conf import settings -from django.contrib.auth.hashers import make_password +"""This module defines API views for user creation""" from rest_framework import status -from rest_framework.permissions import IsAuthenticated, AllowAny +from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework.views import APIView -from rest_framework_simplejwt.tokens import RefreshToken -from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter -from allauth.socialaccount.providers.oauth2.client import OAuth2Client - -from dj_rest_auth.registration.views import SocialLoginView - -from google_auth_oauthlib.flow import InstalledAppFlow - -from .access_token_cache import store_token -from .serializers import MyTokenObtainPairSerializer, CustomUserSerializer -from .managers import CustomAccountManager -from .models import CustomUser - - -class ObtainTokenPairWithCustomView(APIView): - """ - Custom Token Obtain Pair View. - Allows users to obtain access and refresh tokens by providing credentials. - """ - permission_classes = (AllowAny,) - - def post(self, request): - """ - Issue access and refresh tokens in response to a valid login request. - """ - serializer = MyTokenObtainPairSerializer(data=request.data) - if serializer.is_valid(): - token = serializer.validated_data - return Response(token, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) +from .serializers import CustomUserSerializer class CustomUserCreate(APIView): @@ -60,126 +25,3 @@ class CustomUserCreate(APIView): if user: return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -class GreetingView(APIView): - """ - Hello World View. - Returns a greeting and user information for authenticated users. - """ - permission_classes = (IsAuthenticated,) - - def get(self, request): - """ - Retrieve a greeting message and user information. - """ - user = request.user - user_info = { - "username": user.username, - } - response_data = { - "message": "Hello, world!", - "user_info": user_info, - } - return Response(response_data, status=status.HTTP_200_OK) - - -class GoogleLogin(SocialLoginView): - """ - Google Login View. - Handles Google OAuth2 authentication. - """ - # permission_classes = (AllowAny,) - adapter_class = GoogleOAuth2Adapter - # client_class = OAuth2Client - # callback_url = 'http://localhost:8000/accounts/google/login/callback/' - - -class GoogleRetrieveUserInfo(APIView): - """ - Retrieve user information from Google and create a user if not exists. - """ - permission_classes = (AllowAny,) - client_config = {"web":{"client_id": settings.GOOGLE_CLIENT_ID, - "project_id":"turtask","auth_uri": "https://accounts.google.com/o/oauth2/auth", - "token_uri": "https://oauth2.googleapis.com/token", - "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", - "client_secret": settings.GOOGLE_CLIENT_SECRET, - } - } - scopes = [ - 'https://www.googleapis.com/auth/userinfo.email', - 'https://www.googleapis.com/auth/userinfo.profile', - 'https://www.googleapis.com/auth/calendar.readonly', - ] - - def post(self, request): - code = request.data.get("code") - payload = self.exchange_authorization_code(code=code) - if 'error' in payload: - return Response({'error': payload['error']}) - user_info = self.call_google_api(api_url='https://www.googleapis.com/oauth2/v2/userinfo?alt=json', - access_token=payload['access_token']) - payload['email'] = user_info['email'] - user = self.get_or_create_user(payload) - token = RefreshToken.for_user(user) - - response = { - 'username': user.username, - 'access_token': str(token.access_token), - 'refresh_token': str(token), - } - - return Response(response) - - def get(self, request): - """Get authorization url.""" - flow = InstalledAppFlow.from_client_config(client_config=self.client_config, - scopes=self.scopes) - flow.redirect_uri = 'http://localhost:5173/' - authorization_url, state = flow.authorization_url( - access_type='offline', - # include_granted_scopes='true', - ) - return Response({'url': authorization_url}) - - def exchange_authorization_code(self, code): - """Exchange authorization code for access, id, refresh token.""" - url = 'https://oauth2.googleapis.com/token' - payload = { - 'code': code, - 'client_id': settings.GOOGLE_CLIENT_ID, - 'client_secret': settings.GOOGLE_CLIENT_SECRET, - 'redirect_uri': 'postmessage', - 'grant_type': 'authorization_code', - } - response = requests.post(url, data=payload) - return json.loads(response.text) - - def get_or_create_user(self, user_info): - """Get or create a user based on email.""" - try: - user = CustomUser.objects.get(email=user_info['email']) - user.refresh_token = user_info['refresh_token'] - user.save() - except CustomUser.DoesNotExist: - user = CustomUser() - user.username = user_info['email'] - user.password = make_password(CustomAccountManager().make_random_password()) - user.email = user_info['email'] - user.refresh_token = user_info['refresh_token'] - user.save() - store_token(user.id, user_info['access_token'], 'access') - store_token(user.id, user_info['id_token'], 'id') - return user - - def call_google_api(self, api_url, access_token): - """Call Google API with access token.""" - headers = { - 'Authorization': f'Bearer {access_token}' - } - - response = requests.get(api_url, headers=headers) - if response.status_code == 200: - return response.json() - raise Exception('Google API Error', response)