Building a Custom Google Authentication System with Django Rest Framework and ReactJS I
Hello there! đ Today, we are going to embark on a journey to build a custom Google authentication system using Django Rest Framework and ReactJS, without relying on any social third-party plugins. This tutorial will be divided into two parts, focusing on the setup of our Django backend and the subsequent frontend implementation.
Co-authored by Adetola Abiodun
Iâm excited to share this article, co-authored with Adetola Abiodun. Tola is a seasoned full-stack Web/Mobile Engineer. Together, weâve combined our insights to bring you a comprehensive look into building this custom authentication system. We hope you find our collaboration informative and engaging!
You can find the code for this blog on GitHub: Link
Before we begin, make sure you have the following tools and technologies set up:
- Visual Studio Code (Text Editor)
- Python version 3 ++
- Nodejs
- Windows (OS) using WSL and windows terminal
Table of Content
In this part of the tutorial, we will cover the following steps:
- Setup Project Workflow
- Setting up an Authentication System
- Setting up Google APIs
- Adding Google Credentials
- Creating a Custom Google LoginView
- Testing with Postman đ
- Head to Part 2
1. Setup Project Workflow
Letâs start with creating our django project
# Make a Directory for the Project and navigate into it.
mkdir social-login/backend && cd social-login/backend
# Create and activate a Python Virtual Environment
python3 -m venv venv
source venv/bin/activate# Install the corheaders plugin
pip install django-cors-headers# Create a Django Project
django-admin startproject social_login .# Test Run the server
python manage.py runserver
2. Setting up an Authentication System
In the same social_login directory, letâs create an authentication app to handle the authentication workflow on the system:
python manage.py startapp authentication
Add âauthenticationâ to the INSTALLED_APPS
in your settings.py:
DEFAULT_APPS = [
"corsheaders", #new
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
]
CUSTOM_APPS = [
"authentication",
]
INSTALLED_APPS = DEFAULT_APPS + CUSTOM_APPS
MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware",
....
]
now let's create a custom user model in our authentication/models.py using the AbstractUser
class, as shown below
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
# Add any additional fields you want in your custom user model
email = models.CharField(max_length=250, unique=True, null=False, blank=False)
REGISTRATION_CHOICES = [
('email', 'Email'),
('google', 'Google'),
]
registration_method = models.CharField(
max_length=10,
choices=REGISTRATION_CHOICES,
default='email'
)
def __str__(self):
return self.username
- Importing necessary modules The code begins by importing the required modules:
AbstractUser
fromdjango.contrib.auth.models
andmodels
fromdjango.db
. - Defining the custom user model The
User
class is defined, which inherits fromAbstractUser
. This allows the custom user model to have all the fields and functionality provided by the built-in DjangoUser
model. - Adding additional fields In this step, two additional fields are added to the custom user model:
email
: This field is defined as aCharField
with a maximum length of 250 characters. It is set to be unique, meaning each user must have a unique email address. It cannot be null (empty) or blank (whitespace-only).registration_method
: This field is defined as aCharField
with a maximum length of 10 characters. It represents the method of user registration and has two choices: 'email' and 'google'. The default value is set to 'email'.
Next, letâs use the makemigrations
command to create new database migration files for our authentication app
python manage.py makemigrations
and update the settings.py file to specify the custom user model to be used for authentication in our project.
AUTH_USER_MODEL = 'authentication.User'
3. Setting up Google APIs
To enable Google login functionality in our application, we will need to set up an OAuth application through the Google Developers Console Follow the steps below.
1. Create a New Google APIs project
- Go to the Google Developer APIs Console and access the Dashboard.
- Create a new project by clicking on the âNew Projectâ button.
- Provide a name for your project, preferably using your website or app name. This project name will be visible to users when they are redirected to the Google login page.
- Click on âCreateâ to proceed.
2. Next Update the OAuth Consent Screen
- After creating the project, register your app by configuring the OAuth consent screen.
- You only need to provide the âApp name,â âUser support email,â and âEmail addressesâ under the âDeveloper contact informationâ section.
- Click on the âSave and Continueâ button.
3. Create New API Credentials
- Go back to the âDashboardâ and select âCredentialsâ from the left panel.
- Click on the âCreate Credentialsâ button at the top, and choose the âOAuth Client IDâ option from the dropdown menu.
Under âAuthorized JavaScript originsâ, add the following URIs:
Under âAuthorized redirect URIsâ, add the following URIs:
http://localhost:3000/google
On the same page, you will find your Client ID and Client secret under the âCredentialsâ section. Follow the steps below to locate and copy these details:
- Go to the Google Developer APIs Console and access the âCredentialsâ section from the left-hand side menu.
- Look for the section that displays your OAuth Client ID and Client secret.
- Copy the Client ID and Client secret to use them in the next step of your applicationâs configuration.
These credentials are essential for authenticating our application with Google APIs and enabling the Google login functionality. Make sure to keep them secure and avoid sharing them publicly.
4. Adding Google Credentials
Next letâs add our Google credentials to a .env file on our django project in our social_login directory
export GOOGLE_OAUTH2_CLIENT_ID = YOUR_GOOGLE_CLIENT_ID
export GOOGLE_OAUTH2_CLIENT_SECRET = YOUR_GOOGLE_CLIENT_SECRET
next letâs update and point to it our setting.py file
import os
...
# Google OAuth2 settings
BASE_FRONTEND_URL = os.environ.get('DJANGO_BASE_FRONTEND_URL', default='http://localhost:3000')
GOOGLE_OAUTH2_CLIENT_ID = os.environ.get('GOOGLE_OAUTH2_CLIENT_ID')
GOOGLE_OAUTH2_CLIENT_SECRET = os.environ.get('GOOGLE_OAUTH2_CLIENT_SECRET')
5. Creating a Custom Google LoginView
Create a custom Google login functionality in your authentication app's views.py
:
# views.py
from urllib.parse import urlencode
from rest_framework import serializers
from rest_framework.views import APIView
from django.conf import settings
from django.shortcuts import redirect
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework.response import Response
from .mixins import PublicApiMixin, ApiErrorsMixin
from .services import google_get_access_token, google_get_user_info
from apps.authentication.models import User
from apps.authentication.serializers import UserSerializer
def generate_tokens_for_user(user):
"""
Generate access and refresh tokens for the given user
"""
serializer = TokenObtainPairSerializer()
token_data = serializer.get_token(user)
access_token = token_data.access_token
refresh_token = token_data
return access_token, refresh_token
class GoogleLoginApi(PublicApiMixin, ApiErrorsMixin, APIView):
class InputSerializer(serializers.Serializer):
code = serializers.CharField(required=False)
error = serializers.CharField(required=False)
def get(self, request, *args, **kwargs):
input_serializer = self.InputSerializer(data=request.GET)
input_serializer.is_valid(raise_exception=True)
validated_data = input_serializer.validated_data
code = validated_data.get('code')
error = validated_data.get('error')
login_url = f'{settings.BASE_FRONTEND_URL}/login'
if error or not code:
params = urlencode({'error': error})
return redirect(f'{login_url}?{params}')
redirect_uri = f'{settings.BASE_FRONTEND_URL}/google/'
access_token = google_get_access_token(code=code,
redirect_uri=redirect_uri)
user_data = google_get_user_info(access_token=access_token)
try:
user = User.objects.get(email=user_data['email'])
access_token, refresh_token = generate_tokens_for_user(user)
response_data = {
'user': UserSerializer(user).data,
'access_token': str(access_token),
'refresh_token': str(refresh_token)
}
return Response(response_data)
except User.DoesNotExist:
username = user_data['email'].split('@')[0]
first_name = user_data.get('given_name', '')
last_name = user_data.get('family_name', '')
user = User.objects.create(
username=username,
email=user_data['email'],
first_name=first_name,
last_name=last_name,
registration_method='google',
phone_no=None,
referral=None
)
access_token, refresh_token = generate_tokens_for_user(user)
response_data = {
'user': UserSerializer(user).data,
'access_token': str(access_token),
'refresh_token': str(refresh_token)
}
return Response(response_data)
let's also create a serilizers.py file in our authentication app and update it as it is used in our views.py
# serializers.py
from rest_framework import serializers
from .models import User
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['first_name', 'last_name', 'email']
letâs do so also for the mixins.py file and update it as it used in our views.py
# mixins.py
from rest_framework_simplejwt.authentication import JWTAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework import exceptions as rest_exceptions
from django.core.exceptions import ValidationError
from .utils import get_error_message
from apps.authentication.models import User
class ApiAuthMixin:
authentication_classes = (JWTAuthentication, )
permission_classes = (IsAuthenticated, )
class PublicApiMixin:
authentication_classes = ()
permission_classes = ()
class ApiErrorsMixin:
"""
Mixin that transforms Django and Python exceptions into rest_framework ones.
Without the mixin, they return 500 status code which is not desired.
"""
expected_exceptions = {
ValueError: rest_exceptions.ValidationError,
ValidationError: rest_exceptions.ValidationError,
PermissionError: rest_exceptions.PermissionDenied,
User.DoesNotExist: rest_exceptions.NotAuthenticated
}
def handle_exception(self, exc):
if isinstance(exc, tuple(self.expected_exceptions.keys())):
drf_exception_class = self.expected_exceptions[exc.__class__]
drf_exception = drf_exception_class(get_error_message(exc))
return super().handle_exception(drf_exception)
return super().handle_exception(exc)
also for the utils.py file and update it as it is used in our views.py
# utils.py
import requests
from typing import Dict, Any
from django.conf import settings
from django.core.exceptions import ValidationError
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
GOOGLE_ID_TOKEN_INFO_URL = 'https://www.googleapis.com/oauth2/v3/tokeninfo'
GOOGLE_ACCESS_TOKEN_OBTAIN_URL = 'https://oauth2.googleapis.com/token'
GOOGLE_USER_INFO_URL = 'https://www.googleapis.com/oauth2/v3/userinfo'
def generate_tokens_for_user(user):
"""
Generate access and refresh tokens for the given user
"""
serializer = TokenObtainPairSerializer()
token_data = serializer.get_token(user)
access_token = token_data.access_token
refresh_token = token_data
return access_token, refresh_token
def google_get_access_token(*, code: str, redirect_uri: str) -> str:
data = {
'code': code,
'client_id': settings.GOOGLE_OAUTH2_CLIENT_ID,
'client_secret': settings.GOOGLE_OAUTH2_CLIENT_SECRET,
'redirect_uri': redirect_uri,
'grant_type': 'authorization_code'
}
response = requests.post(GOOGLE_ACCESS_TOKEN_OBTAIN_URL, data=data)
if not response.ok:
raise ValidationError('Failed to obtain access token from Google.')
access_token = response.json()['access_token']
return access_token
def google_get_user_info(*, access_token: str) -> Dict[str, Any]:
response = requests.get(
GOOGLE_USER_INFO_URL,
params={'access_token': access_token}
)
if not response.ok:
raise ValidationError('Failed to obtain user info from Google.')
return response.json()
next, letâs update our urls.py
from django.urls import path
from . import views
urlpatterns = [
path("auth/login/google/", GoogleLoginApi.as_view(),
name="login-with-google"),
]
lastly, letâs update our project urls.py and .env file
# socail_login/urls.py
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("api/", include("authentication.urls")),
]
next our .env file
# socail_login/.env
export GOOGLE_OAUTH2_CLIENT_ID = YOUR_GOOGLE_CLIENT_ID
export GOOGLE_OAUTH2_CLIENT_SECRET = YOUR_GOOGLE_CLIENT_SECRET
export DJANGO_BASE_FRONTEND_URL="http://localhost:3000" #new
and then update settings.py
BASE_FRONTEND_URL = os.environ.get('DJANGO_BASE_FRONTEND_URL') # new
# Google OAuth2 settings
GOOGLE_OAUTH2_CLIENT_ID = os.environ.get('DJANGO_GOOGLE_OAUTH2_CLIENT_ID')
GOOGLE_OAUTH2_CLIENT_SECRET = os.environ.get('DJANGO_GOOGLE_OAUTH2_CLIENT_SECRET')
6. Testing with Postman
Test the Google login view using Postman:
http://127.0.0.1:8000/api/v1/auth/login/google/?
code=4%2F0AZEOvhV5qNkTEnISqWASmWy0O6UaivxTrQvfc6RFUF9Hqxuezmz1EH-umQj0TkeJS_98Xw
&scope=email+profile+openid+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&authuser=0&prompt=none
We see get an error,
[
"Failed to obtain access token from Google."
]
You might encounter an error indicating that the access token from Google couldnât be obtained. This is expected due to the expired code in the URL.
In the next section, weâll cover the front-end implementation using ReactJS.
You can find the code for this blog on GitHub: Link
Thanks for Your Time âĽď¸
I hope youâve found this tutorial helpful. If you have any questions or feedback, feel free to reach out. Stay tuned for more tutorials and guides on various development topics!
Where to find Me đ
Here on Medium âĽď¸
You can also find me also đ Github https://github.com/Onlynfk/
Instagram / LinkedIn
Want to Work with me?
If youâre looking for a skilled developer to collaborate with, you can view my portfolio here. Letâs bring your ideas to life together
Here on Medium âĽď¸