DEV Community

qian
qian

Posted on

Integrate Google OAuth2 authentication into the django-rest-framework

backgrounds

  1. front-end and back-end separation, django-rest-framework as the back-end, react as the front-end. This article also applies to other front-end frameworks.
  2. Application has its own account model and token generation, then need to integrate with google oauth2 authentication.

Ideas for implementation

// these two created in back-end
// short as access_url
google_auth_access_url = "https://localhost:8000/google/access/";
// short as callback_url
google_auth_callback_url = "https://localhost:8000/google/callback/";

// these two created in front-end
// short as google_success_page
google_auth_success_page = "https://localhost:5173/google-success";
// short as google_fail_url
google_auth_fail_page = "https://localhost:5173/google-fail";
Enter fullscreen mode Exit fullscreen mode
  1. Open the access_url that written in the back-end in browser.window.open("http://localhost:8000/google/access");

  2. In back-end, when accessing the access_url, will generate google authentication url and redirect to google oauth2 server, Also need to pass callback_url to google oauth2 server, the callback_url is for handling the response from google oauth2 server.

  3. In callback_url, redirect to the google_success_page created in the frontend if credentials were successfully retrieved, otherwise redirect to the google_fail_url also created in the frontend.

  4. In google_success_page, the token passed by query_params will be revalidated, and if the token is invalid, redirected to google_auth_fail_page.

links

Code for reference

pip install google-auth-oauthlib

https://pypi.org/project/google-auth-oauthlib/#description

To make it clearer, I have pasted the imports and common variables used below.

import google_auth_oauthlib.flow
from django.shortcuts import redirect
from django.conf import settings
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status, serializers
import requests
import os
import jwt
from datetime import datetime
from account.views import login_token_logic
from account.serializers import UserSerializer
from account.models import User
from rest_framework.exceptions import ValidationError
from django.http import HttpResponseRedirect
from urllib.parse import urlencode

os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
CLIENT_SECRETS_FILE = "client_secret.json"

SCOPES = [
    "openid",
    "https://www.googleapis.com/auth/userinfo.profile",
    "https://www.googleapis.com/auth/userinfo.email",
    "https://www.googleapis.com/auth/drive.metadata.readonly",
]
GOOGLE_USER_INFO_URL = "https://www.googleapis.com/oauth2/v3/userinfo"
domain = settings.BASE_BACKEND_URL
API_URI = "account/google/callback"
callback_uri = f"{domain}{API_URI}"
Enter fullscreen mode Exit fullscreen mode

Implementing access view that redirects to Google oauth2 server.

This access_view is for access_url 'http://localhost:8000/google/access'.
when you click 'sign in with google' in the frontend application, open access_url in the browser, then it will redirect to the google authentication url created in the access_view.

@api_view(["get"])
def access_view(request):
    try:
        flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
            CLIENT_SECRETS_FILE,
            scopes=SCOPES,
        )
        flow.redirect_uri = callback_uri
        authorization_url, state = flow.authorization_url(
            access_type="offline",
            include_granted_scopes="true",
            prompt="consent",
        )
        request.session["state"] = state

        return redirect(authorization_url)
    except Exception as e:
        return Response(status=status.HTTP_400_BAD_REQUEST)

Enter fullscreen mode Exit fullscreen mode

Implementing the callback view, handling the google oauth2 server response.

There are a few key steps in the callback view:

check query parameters
class InputSerializer(serializers.Serializer):
    code = serializers.CharField(required=False)
    error = serializers.CharField(required=False)
    state = serializers.CharField(required=False)

input_serializer = InputSerializer(data=request.query_params)
input_serializer.is_valid(raise_exception=True)
validated_data = input_serializer.validated_data
code = validated_data.get("code")
error = validated_data.get("error")
state = validated_data.get("state")
if error is not None:
    return Response({"message": error}, status=status.HTTP_400_BAD_REQUEST)
if code is None or state is None:
    return Response(
        {"message": "Missing code or state"}, status=status.HTTP_400_BAD_REQUEST
    )
state = request.session.get("state")
if not state:
    return Response(
        {"message": "not valid request"}, status=status.HTTP_400_BAD_REQUEST
    )
Enter fullscreen mode Exit fullscreen mode
fetch token and get credentials

authorization_response is the full uri that oauth2 response, including the query parameters

def credentials_to_dict(credentials):
    return {
        "token": credentials.token,
        "refresh_token": credentials.refresh_token,
        "id_token": credentials.id_token,
        "token_uri": credentials.token_uri,
        "client_id": credentials.client_id,
        "client_secret": credentials.client_secret,
        "scopes": credentials.scopes,
    }
flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
    CLIENT_SECRETS_FILE, scopes=SCOPES, state=state
)
flow.redirect_uri = callback_uri
authorization_response = request.build_absolute_uri(request.get_full_path())
flow.fetch_token(authorization_response=authorization_response)
credentials = flow.credentials
credentials_dict = credentials_to_dict(credentials)
Enter fullscreen mode Exit fullscreen mode
decode id_token to get expired time and query google account infos
def get_user_info(token):
    response = requests.get(GOOGLE_USER_INFO_URL, params={"access_token": token})

    if not response.ok:
        raise Exception("Failed to obtain user info from Google")

    return response.json()


def decode_id_token(id_token):
    decoded_tokem = jwt.decode(jwt=id_token, options={"verify_signature": False})
    return decoded_tokem
user_info = get_user_info(credentials.token)
user_email = user_info["email"]
id_token_decoded = decode_id_token(credentials.id_token)
exp = id_token_decoded["exp"]
Enter fullscreen mode Exit fullscreen mode
Integrate Google Account with the application's own account model, you should modify this according to your own login logic.

Find out if this Google account already exists in my account model, if not, create one. Then do the login logic, generate token.

now = int(datetime.now().timestamp())

user = User.objects.filter(email=user_email).first()
if user is None:
    # create user
    create_user_data = {
        "email": user_email,
        "image": user_info["picture"],
        "type": "google",
        "password": "",
    }
    create_serializer = UserSerializer(data=create_user_data)
    if create_serializer.is_valid():
        user = create_serializer.save()
    else:
        return Response(
            {
                "message": "create user from google failed",
                **create_serializer.errors,
            },
            status=status.HTTP_400_BAD_REQUEST,
        )
    """
    login logic
    """
token = login_token_logic(user, valid_seconds=exp - now)
Enter fullscreen mode Exit fullscreen mode
redirect to frontend page.

I create two pages in the frontend, one for successful Google sign-in, the other for failure.
success: 'http://localhost:5173/google-success/?token=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
failure: 'http://localhost:5173/google-fail/?message=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'

The success page will revalidate the token, if the token is not valid, will navigate to the failure page.

Again, this step is entirely up to your design.

GOOGLE_OAUTH2_REDIRECT_SUCCESS_URL = "http://localhost:5173/google-success/"
GOOGLE_OAUTH2_REDIRECT_FAIL_URL = "http://localhost:5173/google-fail/"
    query_params = urlencode(
        {
            "token": token,
        }
    )
    url = f"{settings.GOOGLE_OAUTH2_REDIRECT_SUCCESS_URL}?{query_params}"
    return HttpResponseRedirect(url)
except Exception as e:
    query_params = urlencode({"message": str(e)})
    url = f"{settings.GOOGLE_OAUTH2_REDIRECT_FAIL_URL}?{query_params}"
    return HttpResponseRedirect(url)
Enter fullscreen mode Exit fullscreen mode

tips

  • problem: (insecure_transport) OAuth 2 MUST utilize https. solution: os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"

Top comments (0)