Google OAuth2 with Django REST Framework & React: Part 2

Ventsislav Tashev
Martin Angelov
May 11, 2021
Categories:DjangoReact

This article is a follow-up from this one. Here we will migrate the OAuth2 flow from the client to the server.

TL;DR

In this article, we'll show you an implementation of the server-side Google OAuth2 flow.

All of the code examples and the full implementation from this article are placed in this GitHub repository. It's deployed here so you can give it a try before diving into the blog post.

If you've found this useful, don't forget to give us a ā­.

Recap

We've encountered various limitations when we used the client-side OAuth2 flow. We were not able to access the more "sensitive" Google APIs and fetch more protected information for the user. This is expected because Google requires us to perform this in a secure way. The only secure way to do this is by taking the server-side route.

You can read our previous blog post for more info.

Overview

Here is a simple diagram that shows the current implementation that we have (client-side flow):

Google OAuth2 Client-side Flow diagram

Our goal is to transform it into this (server-side flow):

Google OAuth2 Server-side Flow diagram

Before we start: OAuth2 & Django

Logging users in using OAuth2 is a common thing for most websites nowadays. We cannot think of a platform that we've used recently where we couldn't log in using a Google account. That's why we decided to use the Google OAuth2 provider for our examples.

We said that the situation is a little bit messy right now and here is why. If you check the official Django packages for OAuth2 (and this doesn't even include the DRF ones), you'll find out that there are a lot of packages from which you can choose from:

Django OAuth Packages

This is the main reason we've decided to implement the OAuth2 flow on our own. Here are the key points:

We believe that you won't use more than 2 or 3 OAuth2 providers in your app. It's relatively easy to build this infrastructure on your own as we'll show you in the examples below. Consider doing this instead of spending your time debugging why your third-party log-in flow is not working as expected.

Server-side OAuth2

This approach is very similar to the client-side one. The main difference is that you no longer need to use iframes. Here is what you need to do in short:

  1. Your front-end should redirect the user to the official Google OAuth2 page.
  2. When the form is submitted, Google will call your backend via a callback API.
  3. If everything is OK, you should create a new login session for your user.
  4. Redirect your user to the desired page in the front-end.
Google OAuth2 Flow Server-side

Google Application Set-up

For this flow to work properly, you'll need to set your backend OAuth2 callback URL(s) in the Authorized redirect URIs in your Google application settings:

Google OAuth2 application setup
Note: For local Django development, the default server port is 8000. Change this corresponding to your specific environment. If you decide to deploy your app, you just need to add another URI and set the live app's one.

Server-side flow in your React front-end

Once you're ready with your Google Application set-up, you'll have to add a "Google Login" button that redirects your users to the official Google OAuth2 page.

Note: Be careful with the styles of the button!

If you want to use Google OAuth2 in your app you must follow the Google Branding Guidelines!

If you don't want to style it on your own you can just use react-google-button as we do in this example.

Redirecting to the Google OAuth2 page

There is nothing special here - you just need to make a standard JavaScript redirect here. This is usually done by changing the location of the current window.

There are few important things that you should follow here:

Here is an example code snippet that we use for the current example. It can be found here.

const openGoogleLoginPage = useCallback(() => {
  const googleAuthUrl = 'https://accounts.google.com/o/oauth2/v2/auth';
  const redirectUri = 'api/v1/auth/login/google/';

  const scope = [
    'https://www.googleapis.com/auth/userinfo.email',
    'https://www.googleapis.com/auth/userinfo.profile'
  ].join(' ');

  const params = {
    response_type: 'code',
    client_id: REACT_APP_GOOGLE_CLIENT_ID,
    redirect_uri: `${REACT_APP_BASE_BACKEND_URL}/${redirectUri}`,
    prompt: 'select_account',
    access_type: 'offline',
    scope
  };

  const urlParams = new URLSearchParams(params).toString();

  window.location = `${googleAuthUrl}?${urlParams}`;
}, []);

Server-side flow in your DRF backend

We're ready with the front-end side of our implementation. Here is what we've achieved so far:

  1. We have a "Login with Google" button
  2. The users are redirected to the Google OAuth2 page when they press this button

Now we need to define what should happen when your users try to log in using Google. This happens in the Google Login Callback API that you should implement in your backend. As we pointed out above, the URL of this API must be:

Handling Google OAuth2 Callback

Google OAuth2 Example Error Handling
  1. The authentication code expires in 10 minutes!
  2. You should use this code to retrieve the access_token of your user.
  3. Once you have the access_token you can use it to retrieve the data that you need for this user.
  4. NOTE: Check the scope parameter in your front-end code if you have troubles accessing the desired user data.
  5. The most common practice here is to use the email of the authorized user in order to match it to a user from your system. This means that your users must have a unique email in your database!
  6. Once you retrieve your user record from the database, you can create a JWT login session for it.
  7. The last step that you should do is to redirect the user to the desired page in your front-end.

Here is our implementation of the above steps.

from urllib.parse import urlencode

from rest_framework import status, serializers
from rest_framework.views import APIView
from rest_framework.response import Response

from django.urls import reverse
from django.conf import settings
from django.shortcuts import redirect

from api.mixins import ApiErrorsMixin, PublicApiMixin

from users.services import user_get_or_create

from auth.services import jwt_login, google_get_access_token, google_get_user_info


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}')

        domain = settings.BASE_BACKEND_URL
        api_uri = reverse('api:v1:auth:login-with-google')
        redirect_uri = f'{domain}{api_uri}'

        access_token = google_get_access_token(code=code, redirect_uri=redirect_uri)

        user_data = google_get_user_info(access_token=access_token)

        profile_data = {
            'email': user_data['email'],
            'first_name': user_data.get('givenName', ''),
            'last_name': user_data.get('familyName', ''),
        }

        # We use get-or-create logic here for the sake of the example.
        # We don't have a sign-up flow.
        user, _ = user_get_or_create(**profile_data)

        response = redirect(settings.BASE_FRONTEND_URL)
        response = jwt_login(response=response, user=user)

        return response
GoogleLoginApi implementation

Obtaining the access token

To get the access_token of your users, you need to do a POST request to Google using the authentication code from the callback API.

def google_get_access_token(*, code: str, redirect_uri: str) -> str:
    # Reference: https://developers.google.com/identity/protocols/oauth2/web-server#obtainingaccesstokens
    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
google_get_access_token implementation

Apart from the access_token, you can also find a refresh_token and expires_in in the payload here. You can calculate the expiry date of the access_token using the expires_in value (the token expires after expires_in seconds). You can use the refresh_token to "reborn" your access_token when it expires.

We won't go into details about the refreshing of the access tokens but you can read more on the theme in the Google docs.

Here is an example payload that you can use for a reference:

{'access_token': 'ya29.a0AfH6SMC6EcF7IfOEDFm2vBh54vvjk1Rmhp9WIT3tcmItN2hIYhVb4iAhK0OhqFi8urGVQAJMYCL3sb9lRC6wUOqubGwxTVACub3aejXm8ndtneTIR7Hp0BDaGBvY8YjkcMLc3CvLyosOZtnOc9rRdRxAwdsB',
 'expires_in': 3599,
 'id_token': 'eyJhbGciOiJSUzI1NiIsImtpZCI6Ijc3MjA5MTA0Y2NkODkwYTVhZWRkNjczM2UwMjUyZjU0ZTg4MmYxM2MiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI3MzkxMzE1MDc3NTgtaGhwYWg3am1sZDVnNWNyOWYya3QwbmVvbjJsamppcm4uYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI3MzkxMzE1MDc3NTgtaGhwYWg3am1sZDVnNWNyOWYya3QwbmVvbjJsamppcm4uYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMDE4MDQ3ODM2MTU4ODc3MTI3NTQiLCJlbWFpbCI6Im1hcnRpbmFuZ2Vsb3YwNTZAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImF0X2hhc2giOiJWZTZxeUhCSUZNR2o3cWtpZ0xTSUt3IiwibmFtZSI6Ik1hcnRpbiBBbmdlbG92IiwicGljdHVyZSI6Imh0dHBzOi8vbGgzLmdvb2dsZXVzZXJjb250ZW50LmNvbS9hLS9BT2gxNEdoUkZ4RUdRMGlCY0FYWkRwR2QwN3pOaUdVYm0ySEdVNVBfcS05bz1zOTYtYyIsImdpdmVuX25hbWUiOiJNYXJ0aW4iLCJmYW1pbHlfbmFtZSI6IkFuZ2Vsb3YiLCJsb2NhbGUiOiJiZyIsImlhdCI6MTYyMDExNjcwMSwiZXhwIjoxNjIwMTIwMzAxfQ.LgcPS5n6wS9foS4DRE7srljaTGxfgXZONKcyu3UxsyHpdAKF5jOgNKBwy3wf-uKmmBq_5T3o9E8jsEMLvASym7fklkrUYlAoGT4MA0qags4CzqqeNSDDRQVWwJw3AjndO9XG8M8hcQQBfWnggbVX_4eevbvFKVlzX5vDFNC3qbJA9iiKbc8eH9b0dLA2GclXKIjxtAI3nyUVezU6DqS9wNWk8zkHIJf5x-jr_kzm8aHXH6XHplZab54YuGRsOzskrpFAPiSaj5VSSDldoHpXn4Sx81bDtGhTr6FwFT-2-PGQPTfe73j0sB03BVWjdR8O0NQwFK6PhpLab6A-mACMyA',
 'refresh_token': '1//09DuNCsBljo9uCgYIARAAGAkSNwF-L9Irh2Yh4vlt-WOHqBRxY0IY60NAmbVOoYwWCwgIgSU-ShwFVicYtlVbOr9J8WLEGDna00o',
 'scope': 'https://www.googleapis.com/auth/userinfo.email openid '
          'https://www.googleapis.com/auth/userinfo.profile',
 'token_type': 'Bearer'}

NOTE: The refresh_token won't be present in the payload if you didn't set the access_type to offline in the initial phase!

access_token payload

That's all folks! You now have a fully working server-side Google OAuth2 flow in your app! šŸŽ‰

Upsides

Once you've retrieved a valid access token for the user, you can store it in your database & access Google APIs on behalf of the user even when he/she is not present in your application. This is the main downside of the client-side approach.

This can be accomplished only if you have a secure way of storing your users' information. Luckily, with the server-side implementation - you can do that.

From the Google docs:

This OAuth 2.0 flow is specifically for user authorization. It is designed for applications that can store confidential information and maintain state. A properly authorized web server application can access an API while the user interacts with the application or after the user has left the application.

Summary

The OAuth2 flow is one of the most used ways of user log-in in nowadays. It's an essential part of almost all client-facing web apps.

These articles aim to help you through the OAuth2 integration by providing you an end-to-end implementation of both the client-side & server-side flows. We hope this was valuable for you ā¤ļø!

You can find the full implementation here.

Check our blog for more useful articles šŸ¤ .