Implementing Google Login with JavaScript and FastAPI

Implementing Google Login with JavaScript and FastAPI

This article is based on a recent implementation of Google Auth for one of my projects. See in action here: https://zima.farawaytech.com/.

The implementation uses plain JavaScript on the frontend and FastAPI on the backend. It uses modern Google Identity Services library to handle user authentication on the frontend, as the previous library was deprecated some time ago. On the backend, FastAPI verifies the ID token and issues JWT authentication cookie to manage the session.

Frontend Setup

Add Google Identity Services to Your Project

Include the Google Identity Services JavaScript library in your HTML file by adding the following script tag:

<script src="https://www.gstatic.com/identity/one-tap/js/sdk.js"></script>

Create a Login Button

Add a login button to your HTML file:

<div id="g_id_onload"
     data-client_id="YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com"
     data-context="signin"
     data-ux_mode="popup"
     data-callback="handleGoogleUserSignIn"
     data-auto_prompt="false">
</div>

<div class="g_id_signin"
     data-type="standard"
     data-shape="rectangular"
     data-theme="outline"
     data-text="signin_with"
     data-size="large"
     data-logo_alignment="center"
     data-width="342">
</div>

Replace YOUR_GOOGLE_CLIENT_ID with your actual Google client ID. The handleGoogleUserSignIn function will be called when the user successfully signs in with Google. Full reference of all available parameter can be found here: https://developers.google.com/identity/gsi/web/reference/html-reference

Send ID Token to Backend for Verification

Add handler for authentication response from Google: on success it will send the ID token to the FastAPI backend for verification and authentication, and then set the authentication cookie:

function decodeJwtResponse(token) {
    let base64Url = token.split('.')[1];
    let base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
    let jsonPayload = decodeURIComponent(window.atob(base64).split('').map(function (c) {
        return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
    }).join(''));

    return JSON.parse(jsonPayload);
}

function handleGoogleUserSignIn(response) {
    const id_token = response.credential;
    // Send the id_token to your server for verification and authentication
    // Handle successful sign-in as needed
    const responsePayload = decodeJwtResponse(response.credential);

    console.log("ID: " + responsePayload.sub);
    console.log("Image URL: " + responsePayload.picture);
    console.log("Email: " + responsePayload.email);

    // Send the ID token to your backend API for verification and authentication
    fetch('https://your-backend-api.example.com/auth/google', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({id_token: id_token}),
        credentials: 'include' // Include this line to send and receive cookies
    }).then(response => response.json())
        .then(data => {
        if (data.status === 'success') {
            // The authentication was successful, and the cookie has been set
            console.log('Authentication successful');
        } else {
            // There was an error during the authentication process
            console.log('Authentication error:', data.message);
        }
    }).catch(error => {
        console.error('Error:', error);
    });
}

Replace https://your-backend-api.example.com/auth/google with your FastAPI backend's endpoint for handling Google authentication.

Backend Setup

Install Required Libraries

Install the required libraries for your FastAPI backend:

pip install fastapi[all] pyjwt google-auth google-auth-oauthlib

Configure CORS in FastAPI

Don't forget to add CORS middleware to your FastAPI application to allow credentials:

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:8080"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

Create an Endpoint for Google Authentication

Create a FastAPI endpoint to handle Google authentication and verify the ID token:

from fastapi import FastAPI, Request, Response, Depends
from google.oauth2 import id_token
from google.auth.transport import requests
from pydantic import BaseModel
import jwt

app = FastAPI()

GOOGLE_CLIENT_ID = "YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com"
COOKIE_SECRET_KEY = "SECRET_KEY"  # read from env


class Token(BaseModel):
    id_token: str


@app.post("/auth/google")
async def authenticate_google_user(token: Token, response: Response):
    try:
        # Verify the ID token
        idinfo = id_token.verify_oauth2_token(token.id_token, requests.Request(), GOOGLE_CLIENT_ID)
        # ID token is valid
        user_id = idinfo["sub"]
    
        # Issue an authentication cookie
        auth_cookie = jwt.encode({"user_id": user_id}, COOKIE_SECRET_KEY, algorithm="HS256")
        response.set_cookie(key="auth_cookie", value=auth_cookie)    
        return {"status": "success"}

    except ValueError as e:
        # Invalid ID token
        return {"status": "error", "message": str(e)}

Replace YOUR_GOOGLE_CLIENT_ID string with your actual Google client ID and COOKIE_SECRET_KEY with your own secret key for encoding the JWT authentication cookie.

Fetch User Data in the Backend

Now let's see how to use the authentication cookie we set to access private user data:

from fastapi import Cookie

async def get_user_data(auth_cookie: Optional[str] = Cookie(None)):
    if auth_cookie:
        try:
            payload = jwt.decode(auth_cookie, COOKIE_SECRET_KEY, algorithms=["HS256"])
            user_id = payload.get("user_id")
            # Fetch user data from your database or other data sources using the user_id
            return {"status": "success", "data": "user data"}
        except jwt.exceptions.InvalidTokenError:
            return {"status": "error", "message": "Invalid token"}
    else:
        return {"status": "error", "message": "No token provided"}

@app.get("/user-data")
async def user_data_route(user_data: dict = Depends(get_user_data)):
    return user_data

Replace COOKIE_SECRET_KEY with the secret key used to encode the JWT authentication cookie.

Fetch User Data on the Frontend

Use fetch() to send a request to the backend to fetch user data:

function fetchUserData() {
  fetch('https://your-backend-api.example.com/user/data', {
    method: 'GET',
    headers: {
      'Content-Type': 'application/json'
    },
    credentials: 'include'
  })
  .then(response => response.json())
  .then(data => {
    if (data.status !== "error") {
      console.log('User data:', data);
    } else {
      console.log('Error fetching user data:', data.message);
    }
  })
  .catch(error => {
    console.error('Error:', error);
  });
}

Now we have a complete setup for implementing Google authentication with plain JavaScript on the frontend and FastAPI on the backend. Users can sign in with their Google accounts, and you can fetch their data from the backend using JWT authentication cookies.