DEV Community

Cover image for How I integrated Google OAuth in my Reddit clone (DRF + React)
Michael Obasoro
Michael Obasoro

Posted on • Edited on

How I integrated Google OAuth in my Reddit clone (DRF + React)

Google OAuth allows users to sign in to your site using their Google account, instead of creating a new username and having to remember yet another password. It also saves development time and greatly reduces security risks, because you wouldn't have to build registration forms and handle password hashing and storage.

In this article, I'm going to show you how I implemented "Log in/Continue with Google" in my full-stack web application, and how you can too in your project.

Before we proceed, it's worth noting that this article was written with a specific stack in mind. There will be slight differences in implementation details depending on what stack your application uses. If this is a feature you want your application to have, and you happen to be using a different stack, you can still read along. Odds are you'll pick up a thing or two.

Prerequisites

To properly follow this tutorial, you should already have:

  • A running full-stack application (React + Django, or something similar)
  • Basic understanding of how authentication works
  • A Google account

Setting up Google OAuth

  1. For your application to use Google OAuth, you'll need to create an account with Google Cloud Platform if you don't already have one.
  2. Then, create a project on the dashboard. GCP Project Creation GCP Project Creation cntd GCP Project Creation cntd
  3. Navigate to your project's Credentials section under APIs & services. Search for credentials on GCP project dashboard
  4. Create an OAuth application/client by filling all required fields in the form. (In this tutorial, we will be using Google's OAuth Popup UX, so do not add any URL in the "Authorised redirect URIs" section) OAuth client ID creation OAuth client ID creation cntd
  5. Configure the consent screen, then safely take note of the Client ID and secret. You will be needing them both in the next few steps.

Frontend Integration

The Google authentication process on web applications starts at the frontend where a user is presented with a link or button asking if they would like to "Continue with Google". This is the exact behaviour we want in our frontend.
The first step is to install the @react-oauth/google package which makes the integration of Google Login into our application a lot easier:

// npm
npm install @react-oauth/google@latest

// yarn
yarn add @react-oauth/google@latest
Enter fullscreen mode Exit fullscreen mode

Once the installation is complete, wrap your React app with the GoogleOAuthProvider component. The component takes a clientId prop. Provide it with the clientId you obtained from the Google Cloud Console:

// app.jsx

import { GoogleOAuthProvider } from '@react-oauth/google';

const App = () => {
  return (
    <GoogleOAuthProvider clientId="your-client-id">
      {/* App components */}
    </GoogleOAuthProvider>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

Adding the Google Button to your React App

The @react-oauth/google package provides a GoogleLogin component that renders a button and manages the sign-in flow. However, we will opt to use the UseGoogleLogin hook that the package provides to give us finer control over how the button looks and behaves:

// Login.jsx

import { useGoogleLogin } from '@react-oauth/google';
Enter fullscreen mode Exit fullscreen mode

The useGoogleLogin hook takes an "options" object with multiple properties for configuration. In our case, there are just three we need to worry about:

  • onSuccess: A callback function that's called if the Google Authentication succeeds
  • onError: A callback function that's called if an error occurs during Google Authentication
  • flow: A string value to set which flow of Google OAuth we want our application to use

We will use this hook to create a custom 'Log in with Google' button that triggers the OAuth popup and handles the authentication flow:

// Login.jsx
...
...
function Login(){
    const handleGoogleLoginSuccess = async (credentialsResponse) => {
    try {
      // Send authorization-code to the backend server
      const response = await fetch(`${BACKEND_URL}/google-login/`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({'auth_code': credentialsResponse.code})
      });

      if(!response.ok){
        const error = await response.json();
        console.error("Login failed.", error);
      }else{
        const data = await response.json();
        // Handle user login
        login(data);
        // Redirect the user home or to the appropriate page
        navigate("/");
      }
    }catch(err){
        console.error("Google Login failed", err);
    }
  }

  const handleGoogleLoginError = () => {
    // Handle login errors here
    console.log("Google login failed");
  }
  const googleLogin = useGoogleLogin({
    onSuccess: handleGoogleLoginSuccess,
    onError: handleGoogleLoginError,
    flow: 'auth-code' // In this flow, our application will receive an auth-code whenever a user successfully authenticates with Google. We will use this code to complete the authentication process
  })

  return (
    {/* Customize the button and add googleLogin to its click event listener */}
    <button onClick={() => googleLogin()}>
      Continue with Google
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Swapping the Authorization Code for Tokens

Before Google Authentication can be completed, the auth-code has to be sent to Google's authorization server in exchange for tokens. This activity will be carried out on our backend.

To get started, add these two lines in your settings.py file:

# settings.py 

import os

GOOGLE_CLIENT_ID = os.getenv('GOOGLE_CLIENT_ID')
GOOGLE_CLIENT_SECRET = os.getenv('GOOGLE_CLIENT_SECRET') # Your client secret should NEVER be exposed in your codebase. Always use environmental variables
Enter fullscreen mode Exit fullscreen mode

Then in your .env file (assuming you already have one), add both these variables with the actual values obtained from your Google Console:

# .env

GOOGLE_CLIENT_ID='your-google-client-id'
GOOGLE_CLIENT_SECRET='your-google-client-secret'
Enter fullscreen mode Exit fullscreen mode

Because we are using environment variables though a .env file, we're going to install a library that handles loading these variables into our environment so our application can use them.
If you haven't already:

pip install python-dotenv
Enter fullscreen mode Exit fullscreen mode

Then back in your settings.py:

# settings.py

# Import the dotenv function
from dotenv import load_dotenv

# Then call it
load_dotenv()
Enter fullscreen mode Exit fullscreen mode

Now we have to write a view/service that communicates with Google's OAuth 2.0 token endpoint. Luckily for us, there's a Python library that helps us do just that.
google-auth is the Google authentication library for Python. It provides the ability to authenticate to Google APIs using various methods. Install the library:

pip install --upgrade google-auth
Enter fullscreen mode Exit fullscreen mode

Then we write a service that handles the code exchange:

# services.py

from django.conf import settings
from google.oauth2 import id_token
from google.auth.transport import requests as google_requests
import requests

class GoogleAuthService:
    @staticmethod
    def exchange_code_for_tokens(code):
        CLIENT_ID = settings.GOOGLE_CLIENT_ID
        CLIENT_SECRET = settings.GOOGLE_CLIENT_SECRET
        TOKEN_URL = "https://oauth2.googleapis.com/token"

        response = requests.post(
            TOKEN_URL,
            data={
                "code": code, # Passed from the frontend request
                "client_id": CLIENT_ID,
                "client_secret": CLIENT_SECRET,
                "redirect_uri": "postmessage", # A special placeholder that tells Google not to redirect to a URL, but send the auth-code directly to the app. We're doing this because it works great in Single Page Apps where we typically avoid full-page reloads or redirects
                "grant_type": "authorization_code",
            },
        )

        if response.status_code == 200:
            # Google responds with `id_token` and some other tokens with their data
            response_data = response.json()

            try:
                # Google verifies the id_token's authenticity and returns a dictionary of the user's info
                idinfo = id_token.verify_oauth2_token(response_data['id_token'], google_requests.Request(), CLIENT_ID)

                return {
                    # The only user info our implementation requires
                    "email": idinfo.get('email'),
                    "sub": idinfo.get('sub')
                }

            except Exception as e:
                raise ValueError(e)

        else:
            raise Exception(f"Failed to exchange code: {response.text}")
Enter fullscreen mode Exit fullscreen mode

Then in our views.py, we add a view for logging a user in to our application with Google:

# views.py

@api_view(['POST'])
def google_login(request):
    code = json.loads(request.body)['auth_code']

    if not code:
        return Response({'error': 'Token is required'}, status=status.HTTP_400_BAD_REQUEST)

    try:
        user_id_info = GoogleAuthService.exchange_code_for_tokens(code)
        email = user_id_info.get('email')
        # Google has verified that this is a real email address, and its owner has given your application permission to use it
        # Get or Create User object with the email
        # Handle User tokens or session in your application
        return Response({'user': 'some-form-of-user-tokens-and/or-state'})
    except Exception as e:
        return Response({'error': e}) 
Enter fullscreen mode Exit fullscreen mode

To get a better visual of how it all comes together, take a look at my entire google_login view:

@api_view(['POST'])
def google_login(request):
    code = json.loads(request.body)['auth_code']

    if not code:
        return Response({'error': 'Token is required'}, status=status.HTTP_400_BAD_REQUEST)

    try:
        user_id_info = GoogleAuthService.exchange_code_for_tokens(code)
        user_qs = User.objects.filter(
            Q(email=user_id_info.get('email')) | Q(google_sub=user_id_info.get('sub'))
        )

        if user_qs.exists():
            user = user_qs.first()
            if not user.google_sub:
                user.google_sub = user_id_info.get('sub')
                user.save()

            # Generate Tokens
            refresh = RefreshToken.for_user(user)
            return Response(
                {
                    "refresh": str(refresh),
                    "access": str(refresh.access_token),
                    "user": UserSerializer(user, context={"request": request}).data,
                },
                status=status.HTTP_200_OK,
            )
        else:
            return Response({'error': 'There is no account associated with this email.'}, status=404)

    except Exception as e:
        return Response({'error': e})
Enter fullscreen mode Exit fullscreen mode

...and my User model:

# accounts/models.py

class User(AbstractBaseUser, PermissionsMixin, TimeStampedModel):
    username = models.CharField(max_length=200, unique=True, blank=True, null=True)
    email = models.EmailField(_("email address"), unique=True)
    is_staff = models.BooleanField(default=False)
    is_active = models.BooleanField(default=True)
    is_verified = models.BooleanField(default=False)
    date_joined = models.DateTimeField(default=timezone.now)
    last_seen = models.DateTimeField(null=True, blank=True)
    is_first_login = models.BooleanField(default=True)
    google_sub = models.CharField(max_length=100, null=True, blank=True) 

    USERNAME_FIELD = "email"
    REQUIRED_FIELDS = []

    objects = CustomUserManager()

    class Meta:
        verbose_name = "User"
        verbose_name_plural = "Users"

    def __str__(self):
        return self.email

Enter fullscreen mode Exit fullscreen mode

Theoretically, we could put the logic for exchanging tokens directly within our google_login view. However, it's better practice to keep our views focused on handling the request/response cycle and avoid duplicating logic.

"Wait, where did we duplicate logic?" is what you may be thinking. But don't worry, I'll explain in the next paragraph.

"Continuing with Google" isn't just for users who want to log in to your application. They could also be unauthenticated visitors who want to use their Google accounts to register as users on your platform. To handle this, we're going to create a google_register view, and this is what it would look like:

@api_view(['POST'])
def google_register(request):
    # Get authorization token from FE
    code = json.loads(request.body)['auth_code']

    if not code:
        return Response({'error': 'Token is required'}, status=status.HTTP_400_BAD_REQUEST)

    try:
        # We're avoiding code duplication by using our service here
        user_id_info = GoogleAuthService.exchange_code_for_tokens(code)

        if User.objects.filter(
            Q(email=user_id_info.get('email')) | Q(google_sub=user_id_info.get('sub'))
        ).exists():
            return Response({'error': 'An account with this email address already exists. Please proceed to Login'}, status=400)

        new_google_user = User.objects.create(
            email=user_id_info.get('email'),
            google_sub=user_id_info.get('sub'),
            username=generate_random_username()
        )

        return Response(
            {"message": "User registration successful"}, status=status.HTTP_201_CREATED
        )

    except Exception as e:
        return Response({'error': f'User registration failed, {e}'}, status=400)
Enter fullscreen mode Exit fullscreen mode

That's pretty much it.

To recap, we explored how to securely implement Google authentication in a React + Django application using the auth-code flow. I hope this article has been able to provide clarity on how the integration works. If you have any tips or questions, please do not hesitate to leave a comment.

Thanks for reading!

Top comments (2)

Collapse
 
richmirks profile image
Richard Mirks

Thanks for the detailed explanation! I'm curious, did you run into any significant issues with token security or user management during the integration? Also, how difficult would it be to add support for more OAuth providers in this setup?

Collapse
 
mickeycodess profile image
Michael Obasoro

Thanks a lot! And no, not at all.

Adding support for more OAuth providers would be nearly identical to what I did here. It typically involves basic configurations on the provider's dashboard, adding a service/view to the backend to accommodate token exchange, and probably adding a field or two to the User model.