DEV Community

Cover image for A Step-by-Step Guide to Google OAuth2 Authentication with JavaScript and Bun
Idris Akintobi
Idris Akintobi

Posted on

A Step-by-Step Guide to Google OAuth2 Authentication with JavaScript and Bun

When it comes to authenticating users with Google, OAuth2 provides a secure and standardized approach. While there are libraries that abstract much of the process, this article walks you through a foundational implementation using JavaScript and an Express server. By the end, you'll understand the underlying concepts and have a running example of Google OAuth2.

The project can be run with any JavaScript runtime engine like Node or Deno, but we are using Bun (a fast, modern JavaScript runtime) to run our project.


Setting Up Google OAuth2

Before diving into the code, configure your project in the Google Cloud Console.

Step 1: Create a Google Cloud Project

  1. Visit Google Cloud Console.
  2. Create a new project if you do not have an existing one. Create a Google Cloud Project

Step 2: Configure OAuth Consent Screen

  1. Navigate to API & Services > OAuth Consent Screen to configure your application.
  2. Fill in the necessary details like app name and support email. Configure Google OAuth Consent Screen
  3. Customize branding (optional) to provide more details about your app’s consent screen. Customize Google OAuth branding

Step 3: Set Up Scopes

  1. Under the Scopes section, add:
    • email: Access the user’s email address.
    • openid: OpenID Connect scope for identity verification.
    • profile: Access basic profile information like name and picture. Set Up Google OAuth Scopes

Step 4: Create OAuth Client ID

It might take a few minutes before you can create a client ID.

  1. Go to Credentials > Create Credentials > OAuth Client ID.
  2. Select Web Application as the application type.
  3. Set the Authorized JavaScript Origin to http://localhost:3000.
  4. Add a Redirect URI: http://localhost:3000. Replace these with your hosted URI when deploying to production. Create Google OAuth Client ID Google will redirect users to the Redirect URI after authentication, appending an authorization code and state parameters. The redirect URI can be another page or the backend service. For simplicity, we are using the same page to handle the authenticated user code. Google OAuth Client ID

Step 5: Add Test Users

In test mode, you must explicitly add the email addresses of users you want to test the flow with. You should publish your app once you're satisfied with the test.
Add Test Users To Google OAuth Client


Coding the Google OAuth2 Implementation

Project Setup

  1. Install Bun:

    If you don’t have Bun installed, follow the instructions at bun.sh. We are using Bun version 1.2.

  2. Initialize the Project:

    Create a new directory for your project and initialize it with Bun:

   mkdir google-oauth-express
   cd google-oauth-express
   bun init
Enter fullscreen mode Exit fullscreen mode

Folder Structure

Organize your project as follows:

project/
├── public/
│   └── index.html
├── src/
│   └── index.js
├── .env
└── bun.lock
Enter fullscreen mode Exit fullscreen mode
  1. Install Dependencies: Install the required dependencies:
   bun add express google-auth-library
Enter fullscreen mode Exit fullscreen mode
  1. Set Environment Variables: Create a .env file and add:
   GOOGLE_CLIENT_ID=your_google_client_id
   GOOGLE_CLIENT_SECRET=your_google_client_secret
   GOOGLE_REDIRECT_URI=http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

index.html

The HTML file provides a simple interface for initiating Google Sign-In and handling the OAuth redirect.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Google OAuth Test App</title>
    <script>
      // Function to capture URL parameters and handle the OAuth redirect
      function handleOAuthCallback() {
        const urlParams = new URLSearchParams(window.location.search);
        const code = urlParams.get("code");
        const state = urlParams.get("state");

        if (code && state) {
          // Check the CSRF token
          const storedState = localStorage.getItem("oauth_state");
          if (state === storedState) {
            // Send the code to the server for token exchange
            fetch("/auth/google/login", {
              method: "POST",
              headers: {
                "Content-Type": "application/json",
              },
              body: JSON.stringify({ code }),
            })
              .then((response) => response.json())
              .then((data) => {
                console.log("Success:", data);
              })
              .catch((error) => {
                console.error("Error:", error);
              });
          } else {
            console.error("State mismatch");
          }
        }
      }

      // Call the function when the page loads
      handleOAuthCallback();
    </script>
  </head>
  <body>
    <h1>Google OAuth Test App</h1>
    <button id="google-signin-button">Sign in with Google</button>

    <script>
      const GOOGLE_OAUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
      const GOOGLE_CLIENT_ID = "your_google_client_id"; // Replace with your client ID
      const GOOGLE_CALLBACK_URL = "http://localhost:3000";
      const GOOGLE_OAUTH_SCOPES = [
        "https://www.googleapis.com/auth/userinfo.email",
        "https://www.googleapis.com/auth/userinfo.profile",
      ];

      // Function to generate the OAuth URL and redirect the user
      function initiateGoogleOAuth() {
        const state = crypto.randomUUID(); // Generate a CSRF token
        localStorage.setItem("oauth_state", state);

        const params = new URLSearchParams({
          client_id: GOOGLE_CLIENT_ID,
          redirect_uri: GOOGLE_CALLBACK_URL,
          access_type: "offline",
          response_type: "code",
          state: state,
          scope: GOOGLE_OAUTH_SCOPES.join(" "),
        });

        const GOOGLE_OAUTH_CONSENT_SCREEN_URL = `${GOOGLE_OAUTH_URL}?${params.toString()}`;
        window.location.href = GOOGLE_OAUTH_CONSENT_SCREEN_URL;
      }

      // Attach the function to the button click event
      document
        .getElementById("google-signin-button")
        .addEventListener("click", initiateGoogleOAuth);
    </script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

index.js

This file handles the server-side implementation of Google OAuth2.

import express from "express";
import { OAuth2Client } from "google-auth-library";

const app = express();

app.use(express.static("public"));
app.use(express.json());

const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;
const GOOGLE_REDIRECT_URI = process.env.GOOGLE_REDIRECT_URI;

if (!GOOGLE_CLIENT_ID || !GOOGLE_CLIENT_SECRET || !GOOGLE_REDIRECT_URI) {
  throw new Error("Missing required environment variables for Google OAuth.");
}

const googleClient = new OAuth2Client(GOOGLE_CLIENT_ID);

// Middleware to check if the user is authenticated
async function isAuthenticated(req, res, next) {
  const token = req.headers.authorization?.split(" ")[1]; // Extract token from "Bearer <token>"

  if (!token) {
    return res.status(401).json({ message: "Unauthorized: No token provided" });
  }

  try {
    const ticket = await googleClient.verifyIdToken({
      idToken: token,
      audience: GOOGLE_CLIENT_ID,
    });

    const payload = ticket.getPayload();
    req.user = payload;
    next();
  } catch (error) {
    console.error("Token verification failed:", error);
    res.status(401).json({ message: "Unauthorized: Invalid token" });
  }
}

// Route to handle Google OAuth login
app.post("/auth/google/login", async (req, res) => {
  const { code } = req.body;

  if (!code) {
    return res.status(401).json({ message: "Unauthorized" });
  }

  try {
    const response = await fetch("https://oauth2.googleapis.com/token", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        code,
        client_id: GOOGLE_CLIENT_ID,
        client_secret: GOOGLE_CLIENT_SECRET,
        redirect_uri: GOOGLE_REDIRECT_URI,
        grant_type: "authorization_code",
      }),
    }).then((res) => res.json());

    res.json({ token: response.id_token });
  } catch (error) {
    console.error("Error during token exchange:", error);
    res.status(500).json({ message: "Failed to authenticate with Google" });
  }
});

// Protected route example
app.get("/protected", isAuthenticated, (req, res) => {
  res.json({ message: "You are authenticated!", user: req.user });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server is listening on port ${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

Running the Project

  1. Run the Server: Execute the following command:
   bun run src/index.js
Enter fullscreen mode Exit fullscreen mode
  1. Test the Flow:
    • Open http://localhost:3000 in your browser.
    • Click Sign in with Google.
    • Authenticate with your Google account.
    • Check the console for the token.
    • Use the bearer token to access the /protected route.

Conclusion

This implementation provides a solid understanding of Google OAuth2 and can be ported to other frameworks or production-ready applications. While tools like passport or next-auth simplify OAuth2, building it manually ensures you understand how authentication works under the hood.

Top comments (0)