From 22e5b705e3beb8117db14d3f78ccbe0aaa671486 Mon Sep 17 00:00:00 2001 From: sosokker Date: Thu, 2 Nov 2023 00:52:39 +0700 Subject: [PATCH 1/3] Update Use Authorization code to exchange with token. --- backend/core/settings.py | 7 +- backend/users/urls.py | 2 +- backend/users/views.py | 72 +++++++++++++++---- frontend/src/App.jsx | 3 +- frontend/src/api/axiosapi.jsx | 2 +- frontend/src/components/Home.jsx | 11 +++ frontend/src/components/Nav/Navbar.jsx | 3 +- .../authentication/AuthenticationPage.jsx | 11 ++- 8 files changed, 85 insertions(+), 26 deletions(-) create mode 100644 frontend/src/components/Home.jsx diff --git a/backend/core/settings.py b/backend/core/settings.py index f06c5ac..c795b7c 100644 --- a/backend/core/settings.py +++ b/backend/core/settings.py @@ -79,11 +79,14 @@ REST_FRAMEWORK = { REST_USE_JWT = True +GOOGLE_CLIENT_ID = config('GOOGLE_CLIENT_ID', default='fake-client-id') +GOOGLE_CLIENT_SECRET = config('GOOGLE_CLIENT_SECRET', default='fake-client-secret') + SOCIALACCOUNT_PROVIDERS = { 'google': { 'APP': { - 'client_id': config('GOOGLE_CLIENT_ID', default='fake-client-id'), - 'secret': config('GOOGLE_CLIENT_SECRET', default='fake-client-secret'), + 'client_id': GOOGLE_CLIENT_ID, + 'secret': GOOGLE_CLIENT_SECRET, 'key': '' }, "SCOPE": [ diff --git a/backend/users/urls.py b/backend/users/urls.py index fd03be1..28f77da 100644 --- a/backend/users/urls.py +++ b/backend/users/urls.py @@ -9,5 +9,5 @@ urlpatterns = [ 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()) + 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 cb63de2..3ffb3e9 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -3,6 +3,7 @@ import json import requests +from django.conf import settings from django.contrib.auth.hashers import make_password from rest_framework import status @@ -16,6 +17,8 @@ from allauth.socialaccount.providers.oauth2.client import OAuth2Client from dj_rest_auth.registration.views import SocialLoginView +from google_auth_oauthlib.flow import InstalledAppFlow + from .serializers import MyTokenObtainPairSerializer, CustomUserSerializer from .managers import CustomAccountManager from .models import CustomUser @@ -96,17 +99,28 @@ 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): - access_token = request.data.get("token") - - user_info = self.get_google_user_info(access_token) - - if 'error' in user_info: - error_message = 'Wrong Google token or the token has expired.' - return Response({'message': error_message, 'error': user_info['error']}) - - user = self.get_or_create_user(user_info) + 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 = { @@ -117,13 +131,32 @@ class GoogleRetrieveUserInfo(APIView): return Response(response) - def get_google_user_info(self, access_token): - url = 'https://www.googleapis.com/oauth2/v2/userinfo' - payload = {'access_token': access_token} - response = requests.get(url, params=payload) + 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']) except CustomUser.DoesNotExist: @@ -132,4 +165,15 @@ class GoogleRetrieveUserInfo(APIView): user.password = make_password(CustomAccountManager().make_random_password()) user.email = user_info['email'] user.save() - return user \ No newline at end of file + 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/frontend/src/App.jsx b/frontend/src/App.jsx index 8c7d50d..7333c4a 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -6,6 +6,7 @@ import IconSideNav from './components/IconSideNav'; import AuthenticantionPage from './components/authentication/AuthenticationPage'; import SignUpPage from './components/authentication/SignUpPage'; import NavBar from './components/Nav/Navbar'; +import Home from './components/Home'; const App = () => { @@ -14,7 +15,7 @@ const App = () => {
-

This is Home page!

} /> + }/> }/> }/> }/> diff --git a/frontend/src/api/axiosapi.jsx b/frontend/src/api/axiosapi.jsx index 211cf14..313d6f1 100644 --- a/frontend/src/api/axiosapi.jsx +++ b/frontend/src/api/axiosapi.jsx @@ -67,7 +67,7 @@ const googleLogin = async (token) => { let res = await axios.post( "http://localhost:8000/api/auth/google/", { - token: token, + code: token, } ); // console.log('service google login res: ', res); diff --git a/frontend/src/components/Home.jsx b/frontend/src/components/Home.jsx new file mode 100644 index 0000000..3089df5 --- /dev/null +++ b/frontend/src/components/Home.jsx @@ -0,0 +1,11 @@ +import React from 'react'; + +function HomePage() { + return ( +
+

Welcome to My Website

+
+ ); +} + +export default HomePage; diff --git a/frontend/src/components/Nav/Navbar.jsx b/frontend/src/components/Nav/Navbar.jsx index 331ab9e..85ec720 100644 --- a/frontend/src/components/Nav/Navbar.jsx +++ b/frontend/src/components/Nav/Navbar.jsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Link } from 'react-router-dom'; +import { Link, useNavigate } from 'react-router-dom'; import IsAuthenticated from '../authentication/IsAuthenticated'; import axiosapi from '../../api/axiosapi'; import AppBar from '@mui/material/AppBar'; @@ -28,6 +28,7 @@ const settings = { }; function NavBar() { + const Navigate = useNavigate(); const [anchorElNav, setAnchorElNav] = React.useState(null); const [anchorElUser, setAnchorElUser] = React.useState(null); diff --git a/frontend/src/components/authentication/AuthenticationPage.jsx b/frontend/src/components/authentication/AuthenticationPage.jsx index 791cdd9..a117889 100644 --- a/frontend/src/components/authentication/AuthenticationPage.jsx +++ b/frontend/src/components/authentication/AuthenticationPage.jsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useGoogleLogin } from '@react-oauth/google'; - +import axios from 'axios'; import Avatar from '@mui/material/Avatar'; import Button from '@mui/material/Button'; import CssBaseline from '@mui/material/CssBaseline'; @@ -91,12 +91,11 @@ export default function SignInSide() { } const googleLoginImplicit = useGoogleLogin({ - // flow: 'auth-code', - onSuccess: async (response) => { - console.log(response); - + flow: 'auth-code', + redirect_uri: 'postmessage', + onSuccess: async (response) => { try { - const loginResponse = await axiosapi.googleLogin(response.access_token); + const loginResponse = await axiosapi.googleLogin(response.code); if (loginResponse && loginResponse.data) { const { access_token, refresh_token } = loginResponse.data; From fafb8cf5f96712b01187ab8f9278f3a98484df88 Mon Sep 17 00:00:00 2001 From: sosokker Date: Thu, 2 Nov 2023 01:03:45 +0700 Subject: [PATCH 2/3] Add Save refresh_token in user db --- .../0002_customuser_refresh_token.py | 18 ++++++++++++++++++ backend/users/models.py | 3 +++ backend/users/views.py | 3 +++ 3 files changed, 24 insertions(+) create mode 100644 backend/users/migrations/0002_customuser_refresh_token.py diff --git a/backend/users/migrations/0002_customuser_refresh_token.py b/backend/users/migrations/0002_customuser_refresh_token.py new file mode 100644 index 0000000..25d9215 --- /dev/null +++ b/backend/users/migrations/0002_customuser_refresh_token.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.6 on 2023-11-01 17:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='customuser', + name='refresh_token', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/backend/users/models.py b/backend/users/models.py index e473572..64d976d 100644 --- a/backend/users/models.py +++ b/backend/users/models.py @@ -19,6 +19,9 @@ class CustomUser(AbstractBaseUser, PermissionsMixin): # Custom manager objects = CustomAccountManager() + # Google API + refresh_token = models.CharField(max_length=255, blank=True, null=True) + # Fields for authentication USERNAME_FIELD = 'email' REQUIRED_FIELDS = ['username', 'first_name'] diff --git a/backend/users/views.py b/backend/users/views.py index 3ffb3e9..591c609 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -159,11 +159,14 @@ class GoogleRetrieveUserInfo(APIView): """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() return user From bdfa1bc68e0a9630bd478e55cd0869a8aaacd759 Mon Sep 17 00:00:00 2001 From: sosokker Date: Thu, 2 Nov 2023 01:58:04 +0700 Subject: [PATCH 3/3] Add Refress access token / Improve Authenticate State --- frontend/src/components/Nav/Navbar.jsx | 2 +- .../authentication/AuthenticationPage.jsx | 28 +++++------- .../authentication/IsAuthenticated.jsx | 43 +++---------------- .../components/authentication/SignUpPage.jsx | 4 +- .../authentication/refreshAcessToken.jsx | 37 ++++++++++++++++ 5 files changed, 57 insertions(+), 57 deletions(-) create mode 100644 frontend/src/components/authentication/refreshAcessToken.jsx diff --git a/frontend/src/components/Nav/Navbar.jsx b/frontend/src/components/Nav/Navbar.jsx index 85ec720..f88d821 100644 --- a/frontend/src/components/Nav/Navbar.jsx +++ b/frontend/src/components/Nav/Navbar.jsx @@ -52,7 +52,7 @@ function NavBar() { const logout = () => { // Log out the user, clear tokens, and navigate to the "/testAuth" route axiosapi.apiUserLogout(); - Navigate('/testAuth'); + Navigate('/'); } return ( diff --git a/frontend/src/components/authentication/AuthenticationPage.jsx b/frontend/src/components/authentication/AuthenticationPage.jsx index a117889..0955342 100644 --- a/frontend/src/components/authentication/AuthenticationPage.jsx +++ b/frontend/src/components/authentication/AuthenticationPage.jsx @@ -1,7 +1,6 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useGoogleLogin } from '@react-oauth/google'; -import axios from 'axios'; import Avatar from '@mui/material/Avatar'; import Button from '@mui/material/Button'; import CssBaseline from '@mui/material/CssBaseline'; @@ -17,6 +16,7 @@ import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; import Typography from '@mui/material/Typography'; import { createTheme, ThemeProvider } from '@mui/material/styles'; +import refreshAccessToken from './refreshAcesstoken'; import axiosapi from '../../api/axiosapi'; @@ -24,8 +24,8 @@ function Copyright(props) { return ( {'Copyright © '} - - Your Website + + TurTask {' '} {new Date().getFullYear()} {'.'} @@ -40,6 +40,12 @@ export default function SignInSide() { const Navigate = useNavigate(); + useEffect(() => { + if (!refreshAccessToken()) { + Navigate("/"); + } + }, []); + const [email, setEmail] = useState(""); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); @@ -76,20 +82,6 @@ export default function SignInSide() { }); } - const responseGoogle = async (response) => { - // Handle Google login response - let googleResponse = await axiosapi.googleLogin(response.access_token); - console.log('Google Response:\n', googleResponse); - - if (googleResponse.status === 200) { - // Store Google login tokens and set the authorization header on success - localStorage.setItem('access_token', googleResponse.data.access_token); - localStorage.setItem('refresh_token', googleResponse.data.refresh_token); - axiosapi.axiosInstance.defaults.headers['Authorization'] = "Bearer " + googleResponse.data.access_token; - Navigate('/'); - } - } - const googleLoginImplicit = useGoogleLogin({ flow: 'auth-code', redirect_uri: 'postmessage', diff --git a/frontend/src/components/authentication/IsAuthenticated.jsx b/frontend/src/components/authentication/IsAuthenticated.jsx index e3691c8..48322de 100644 --- a/frontend/src/components/authentication/IsAuthenticated.jsx +++ b/frontend/src/components/authentication/IsAuthenticated.jsx @@ -1,48 +1,19 @@ import { useState, useEffect } from 'react'; -import axiosapi from '../../api/axiosapi'; function IsAuthenticated() { const [isAuthenticated, setIsAuthenticated] = useState(false); useEffect(() => { - const checkAuthentication = async () => { - const access_token = localStorage.getItem('access_token'); - const refresh_token = localStorage.getItem('refresh_token'); + const access_token = localStorage.getItem('access_token'); - if (access_token && refresh_token) { - const isAccessTokenExpired = checkIfAccessTokenExpired(access_token); - - if (!isAccessTokenExpired) { - setIsAuthenticated(true); - } else { - try { - // Attempt to refresh the access token using the refresh token - const response = await axiosapi.refreshAccessToken(refresh_token); - if (response.status === 200) { - const newAccessToken = response.data.access_token; - localStorage.setItem('access_token', newAccessToken); - setIsAuthenticated(true); - } else { - setIsAuthenticated(false); - } - } catch (error) { - setIsAuthenticated(false); - } - } - } else { - setIsAuthenticated(false); - } - }; - - checkAuthentication(); + if (access_token) { + setIsAuthenticated(true); + } else { + setIsAuthenticated(false); + } }, []); - const checkIfAccessTokenExpired = (accessToken) => { - // Need to change logic again! - return !accessToken; - }; - return isAuthenticated; } -export default IsAuthenticated; +export default IsAuthenticated; \ No newline at end of file diff --git a/frontend/src/components/authentication/SignUpPage.jsx b/frontend/src/components/authentication/SignUpPage.jsx index 3e33a86..7739016 100644 --- a/frontend/src/components/authentication/SignUpPage.jsx +++ b/frontend/src/components/authentication/SignUpPage.jsx @@ -21,8 +21,8 @@ function Copyright(props) { return ( {'Copyright © '} - - Your Website + + TurTask {' '} {new Date().getFullYear()} {'.'} diff --git a/frontend/src/components/authentication/refreshAcessToken.jsx b/frontend/src/components/authentication/refreshAcessToken.jsx new file mode 100644 index 0000000..89204d5 --- /dev/null +++ b/frontend/src/components/authentication/refreshAcessToken.jsx @@ -0,0 +1,37 @@ +import axios from 'axios'; + +async function refreshAccessToken() { + const refresh_token = localStorage.getItem('refresh_token'); + const access_token = localStorage.getItem('access_token'); + + if (access_token) { + return true; + } + + if (!refresh_token) { + return false; + } + + const refreshUrl = 'http://127.0.0.1:8000/api/token/refresh/'; + + try { + const response = await axios.post(refreshUrl, { refresh: refresh_token }); + + if (response.status === 200) { + // Successful refresh - save the new access token and refresh token + const newAccessToken = response.data.access; + const newRefreshToken = response.data.refresh; + + localStorage.setItem('access_token', newAccessToken); + localStorage.setItem('refresh_token', newRefreshToken); + + return true; + } else { + return false; + } + } catch (error) { + return false; + } +} + +export default refreshAccessToken;