Building a Custom Google Authentication System with Django Rest Framework and ReactJS I

OnlyGod Ovbije
CodeX
Published in
8 min readAug 7, 2023
Photo by Behnam Norouzi on Unsplash

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:

Table of Content

In this part of the tutorial, we will cover the following steps:

  1. Setup Project Workflow
  2. Setting up an Authentication System
  3. Setting up Google APIs
  4. Adding Google Credentials
  5. Creating a Custom Google LoginView
  6. Testing with Postman 🚀
  7. 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 from django.contrib.auth.models and models from django.db.
  • Defining the custom user model The User class is defined, which inherits from AbstractUser. This allows the custom user model to have all the fields and functionality provided by the built-in Django User model.
  • Adding additional fields In this step, two additional fields are added to the custom user model:
  • email: This field is defined as a CharField 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 a CharField 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:

Click on Create credentails
Fill in Javascript origins and Redirect URL

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:

  1. Go to the Google Developer APIs Console and access the “Credentials” section from the left-hand side menu.
  2. Look for the section that displays your OAuth Client ID and Client secret.
  3. 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
Postman testing

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 ♥️

--

--