mirror of
https://github.com/TurTaskProject/TurTaskWeb.git
synced 2025-12-19 14:04:07 +01:00
Merge pull request #14 from TurTaskProject/feature/user-authentication
Use code authorization to exchange for token / Improve Authenticate Status
This commit is contained in:
commit
aec8a599f9
@ -79,11 +79,14 @@ REST_FRAMEWORK = {
|
|||||||
|
|
||||||
REST_USE_JWT = True
|
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 = {
|
SOCIALACCOUNT_PROVIDERS = {
|
||||||
'google': {
|
'google': {
|
||||||
'APP': {
|
'APP': {
|
||||||
'client_id': config('GOOGLE_CLIENT_ID', default='fake-client-id'),
|
'client_id': GOOGLE_CLIENT_ID,
|
||||||
'secret': config('GOOGLE_CLIENT_SECRET', default='fake-client-secret'),
|
'secret': GOOGLE_CLIENT_SECRET,
|
||||||
'key': ''
|
'key': ''
|
||||||
},
|
},
|
||||||
"SCOPE": [
|
"SCOPE": [
|
||||||
|
|||||||
18
backend/users/migrations/0002_customuser_refresh_token.py
Normal file
18
backend/users/migrations/0002_customuser_refresh_token.py
Normal file
@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -19,6 +19,9 @@ class CustomUser(AbstractBaseUser, PermissionsMixin):
|
|||||||
# Custom manager
|
# Custom manager
|
||||||
objects = CustomAccountManager()
|
objects = CustomAccountManager()
|
||||||
|
|
||||||
|
# Google API
|
||||||
|
refresh_token = models.CharField(max_length=255, blank=True, null=True)
|
||||||
|
|
||||||
# Fields for authentication
|
# Fields for authentication
|
||||||
USERNAME_FIELD = 'email'
|
USERNAME_FIELD = 'email'
|
||||||
REQUIRED_FIELDS = ['username', 'first_name']
|
REQUIRED_FIELDS = ['username', 'first_name']
|
||||||
|
|||||||
@ -9,5 +9,5 @@ urlpatterns = [
|
|||||||
path('token/custom_obtain/', ObtainTokenPairWithCustomView.as_view(), name='token_create_custom'),
|
path('token/custom_obtain/', ObtainTokenPairWithCustomView.as_view(), name='token_create_custom'),
|
||||||
path('hello/', GreetingView.as_view(), name='hello_world'),
|
path('hello/', GreetingView.as_view(), name='hello_world'),
|
||||||
path('dj-rest-auth/google/', GoogleLogin.as_view(), name="google_login"),
|
path('dj-rest-auth/google/', GoogleLogin.as_view(), name="google_login"),
|
||||||
path('auth/google/', GoogleRetrieveUserInfo.as_view())
|
path('auth/google/', GoogleRetrieveUserInfo.as_view()),
|
||||||
]
|
]
|
||||||
@ -3,6 +3,7 @@
|
|||||||
import json
|
import json
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.auth.hashers import make_password
|
from django.contrib.auth.hashers import make_password
|
||||||
|
|
||||||
from rest_framework import status
|
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 dj_rest_auth.registration.views import SocialLoginView
|
||||||
|
|
||||||
|
from google_auth_oauthlib.flow import InstalledAppFlow
|
||||||
|
|
||||||
from .serializers import MyTokenObtainPairSerializer, CustomUserSerializer
|
from .serializers import MyTokenObtainPairSerializer, CustomUserSerializer
|
||||||
from .managers import CustomAccountManager
|
from .managers import CustomAccountManager
|
||||||
from .models import CustomUser
|
from .models import CustomUser
|
||||||
@ -96,17 +99,28 @@ class GoogleRetrieveUserInfo(APIView):
|
|||||||
Retrieve user information from Google and create a user if not exists.
|
Retrieve user information from Google and create a user if not exists.
|
||||||
"""
|
"""
|
||||||
permission_classes = (AllowAny,)
|
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):
|
def post(self, request):
|
||||||
access_token = request.data.get("token")
|
code = request.data.get("code")
|
||||||
|
payload = self.exchange_authorization_code(code=code)
|
||||||
user_info = self.get_google_user_info(access_token)
|
if 'error' in payload:
|
||||||
|
return Response({'error': payload['error']})
|
||||||
if 'error' in user_info:
|
user_info = self.call_google_api(api_url='https://www.googleapis.com/oauth2/v2/userinfo?alt=json',
|
||||||
error_message = 'Wrong Google token or the token has expired.'
|
access_token=payload['access_token'])
|
||||||
return Response({'message': error_message, 'error': user_info['error']})
|
payload['email'] = user_info['email']
|
||||||
|
user = self.get_or_create_user(payload)
|
||||||
user = self.get_or_create_user(user_info)
|
|
||||||
token = RefreshToken.for_user(user)
|
token = RefreshToken.for_user(user)
|
||||||
|
|
||||||
response = {
|
response = {
|
||||||
@ -117,19 +131,52 @@ class GoogleRetrieveUserInfo(APIView):
|
|||||||
|
|
||||||
return Response(response)
|
return Response(response)
|
||||||
|
|
||||||
def get_google_user_info(self, access_token):
|
def get(self, request):
|
||||||
url = 'https://www.googleapis.com/oauth2/v2/userinfo'
|
"""Get authorization url."""
|
||||||
payload = {'access_token': access_token}
|
flow = InstalledAppFlow.from_client_config(client_config=self.client_config,
|
||||||
response = requests.get(url, params=payload)
|
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)
|
return json.loads(response.text)
|
||||||
|
|
||||||
def get_or_create_user(self, user_info):
|
def get_or_create_user(self, user_info):
|
||||||
|
"""Get or create a user based on email."""
|
||||||
try:
|
try:
|
||||||
user = CustomUser.objects.get(email=user_info['email'])
|
user = CustomUser.objects.get(email=user_info['email'])
|
||||||
|
user.refresh_token = user_info['refresh_token']
|
||||||
|
user.save()
|
||||||
except CustomUser.DoesNotExist:
|
except CustomUser.DoesNotExist:
|
||||||
user = CustomUser()
|
user = CustomUser()
|
||||||
user.username = user_info['email']
|
user.username = user_info['email']
|
||||||
user.password = make_password(CustomAccountManager().make_random_password())
|
user.password = make_password(CustomAccountManager().make_random_password())
|
||||||
user.email = user_info['email']
|
user.email = user_info['email']
|
||||||
|
user.refresh_token = user_info['refresh_token']
|
||||||
user.save()
|
user.save()
|
||||||
return user
|
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)
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import IconSideNav from './components/IconSideNav';
|
|||||||
import AuthenticantionPage from './components/authentication/AuthenticationPage';
|
import AuthenticantionPage from './components/authentication/AuthenticationPage';
|
||||||
import SignUpPage from './components/authentication/SignUpPage';
|
import SignUpPage from './components/authentication/SignUpPage';
|
||||||
import NavBar from './components/Nav/Navbar';
|
import NavBar from './components/Nav/Navbar';
|
||||||
|
import Home from './components/Home';
|
||||||
|
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
@ -14,7 +15,7 @@ const App = () => {
|
|||||||
<div className="App">
|
<div className="App">
|
||||||
<NavBar/>
|
<NavBar/>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path={"/"} render={() => <h1>This is Home page!</h1>} />
|
<Route path="/" element={<Home/>}/>
|
||||||
<Route path="/login" element={<AuthenticantionPage/>}/>
|
<Route path="/login" element={<AuthenticantionPage/>}/>
|
||||||
<Route path="/signup" element={<SignUpPage/>}/>
|
<Route path="/signup" element={<SignUpPage/>}/>
|
||||||
<Route path="/testAuth" element={<TestAuth/>}/>
|
<Route path="/testAuth" element={<TestAuth/>}/>
|
||||||
|
|||||||
@ -67,7 +67,7 @@ const googleLogin = async (token) => {
|
|||||||
let res = await axios.post(
|
let res = await axios.post(
|
||||||
"http://localhost:8000/api/auth/google/",
|
"http://localhost:8000/api/auth/google/",
|
||||||
{
|
{
|
||||||
token: token,
|
code: token,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
// console.log('service google login res: ', res);
|
// console.log('service google login res: ', res);
|
||||||
|
|||||||
11
frontend/src/components/Home.jsx
Normal file
11
frontend/src/components/Home.jsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function HomePage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Welcome to My Website</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HomePage;
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import IsAuthenticated from '../authentication/IsAuthenticated';
|
import IsAuthenticated from '../authentication/IsAuthenticated';
|
||||||
import axiosapi from '../../api/axiosapi';
|
import axiosapi from '../../api/axiosapi';
|
||||||
import AppBar from '@mui/material/AppBar';
|
import AppBar from '@mui/material/AppBar';
|
||||||
@ -28,6 +28,7 @@ const settings = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function NavBar() {
|
function NavBar() {
|
||||||
|
const Navigate = useNavigate();
|
||||||
const [anchorElNav, setAnchorElNav] = React.useState(null);
|
const [anchorElNav, setAnchorElNav] = React.useState(null);
|
||||||
const [anchorElUser, setAnchorElUser] = React.useState(null);
|
const [anchorElUser, setAnchorElUser] = React.useState(null);
|
||||||
|
|
||||||
@ -51,7 +52,7 @@ function NavBar() {
|
|||||||
const logout = () => {
|
const logout = () => {
|
||||||
// Log out the user, clear tokens, and navigate to the "/testAuth" route
|
// Log out the user, clear tokens, and navigate to the "/testAuth" route
|
||||||
axiosapi.apiUserLogout();
|
axiosapi.apiUserLogout();
|
||||||
Navigate('/testAuth');
|
Navigate('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useGoogleLogin } from '@react-oauth/google';
|
import { useGoogleLogin } from '@react-oauth/google';
|
||||||
|
|
||||||
import Avatar from '@mui/material/Avatar';
|
import Avatar from '@mui/material/Avatar';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import CssBaseline from '@mui/material/CssBaseline';
|
import CssBaseline from '@mui/material/CssBaseline';
|
||||||
@ -17,6 +16,7 @@ import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
|
|||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { createTheme, ThemeProvider } from '@mui/material/styles';
|
import { createTheme, ThemeProvider } from '@mui/material/styles';
|
||||||
|
|
||||||
|
import refreshAccessToken from './refreshAcesstoken';
|
||||||
import axiosapi from '../../api/axiosapi';
|
import axiosapi from '../../api/axiosapi';
|
||||||
|
|
||||||
|
|
||||||
@ -24,8 +24,8 @@ function Copyright(props) {
|
|||||||
return (
|
return (
|
||||||
<Typography variant="body2" color="text.secondary" align="center" {...props}>
|
<Typography variant="body2" color="text.secondary" align="center" {...props}>
|
||||||
{'Copyright © '}
|
{'Copyright © '}
|
||||||
<Link color="inherit" href="https://mui.com/">
|
<Link color="inherit" href="https://github.com/TurTaskProject/TurTaskWeb">
|
||||||
Your Website
|
TurTask
|
||||||
</Link>{' '}
|
</Link>{' '}
|
||||||
{new Date().getFullYear()}
|
{new Date().getFullYear()}
|
||||||
{'.'}
|
{'.'}
|
||||||
@ -40,6 +40,12 @@ export default function SignInSide() {
|
|||||||
|
|
||||||
const Navigate = useNavigate();
|
const Navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!refreshAccessToken()) {
|
||||||
|
Navigate("/");
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
@ -76,27 +82,12 @@ 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({
|
const googleLoginImplicit = useGoogleLogin({
|
||||||
// flow: 'auth-code',
|
flow: 'auth-code',
|
||||||
|
redirect_uri: 'postmessage',
|
||||||
onSuccess: async (response) => {
|
onSuccess: async (response) => {
|
||||||
console.log(response);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const loginResponse = await axiosapi.googleLogin(response.access_token);
|
const loginResponse = await axiosapi.googleLogin(response.code);
|
||||||
if (loginResponse && loginResponse.data) {
|
if (loginResponse && loginResponse.data) {
|
||||||
const { access_token, refresh_token } = loginResponse.data;
|
const { access_token, refresh_token } = loginResponse.data;
|
||||||
|
|
||||||
|
|||||||
@ -1,47 +1,18 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import axiosapi from '../../api/axiosapi';
|
|
||||||
|
|
||||||
function IsAuthenticated() {
|
function IsAuthenticated() {
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkAuthentication = async () => {
|
const access_token = localStorage.getItem('access_token');
|
||||||
const access_token = localStorage.getItem('access_token');
|
|
||||||
const refresh_token = localStorage.getItem('refresh_token');
|
|
||||||
|
|
||||||
if (access_token && refresh_token) {
|
if (access_token) {
|
||||||
const isAccessTokenExpired = checkIfAccessTokenExpired(access_token);
|
setIsAuthenticated(true);
|
||||||
|
} else {
|
||||||
if (!isAccessTokenExpired) {
|
setIsAuthenticated(false);
|
||||||
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();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const checkIfAccessTokenExpired = (accessToken) => {
|
|
||||||
// Need to change logic again!
|
|
||||||
return !accessToken;
|
|
||||||
};
|
|
||||||
|
|
||||||
return isAuthenticated;
|
return isAuthenticated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -21,8 +21,8 @@ function Copyright(props) {
|
|||||||
return (
|
return (
|
||||||
<Typography variant="body2" color="text.secondary" align="center" {...props}>
|
<Typography variant="body2" color="text.secondary" align="center" {...props}>
|
||||||
{'Copyright © '}
|
{'Copyright © '}
|
||||||
<Link color="inherit" href="https://mui.com/">
|
<Link color="inherit" href="https://github.com/TurTaskProject/TurTaskWeb">
|
||||||
Your Website
|
TurTask
|
||||||
</Link>{' '}
|
</Link>{' '}
|
||||||
{new Date().getFullYear()}
|
{new Date().getFullYear()}
|
||||||
{'.'}
|
{'.'}
|
||||||
|
|||||||
37
frontend/src/components/authentication/refreshAcessToken.jsx
Normal file
37
frontend/src/components/authentication/refreshAcessToken.jsx
Normal file
@ -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;
|
||||||
Loading…
Reference in New Issue
Block a user